@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.
- package/dist/cjs/config-store.js +294 -54
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/mcp-client.js +77 -24
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/config-store.d.ts +7 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +294 -54
- package/dist/config-store.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/mcp-client.d.ts +5 -2
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +77 -24
- package/dist/mcp-client.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +385 -0
- package/src/config-store.ts +408 -74
- package/src/index.ts +4 -1
- package/src/mcp-client.test.ts +80 -0
- package/src/mcp-client.ts +114 -31
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { exchangeCodeForTokens, refreshAccessToken } from "./mcp-client.js";
|
|
3
|
+
|
|
4
|
+
describe("exchangeCodeForTokens", () => {
|
|
5
|
+
it("uses HTTP Basic auth for client_secret_basic", async () => {
|
|
6
|
+
const fetch = vi.fn(async () =>
|
|
7
|
+
Response.json({ access_token: "at", refresh_token: "rt" }),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
await exchangeCodeForTokens(
|
|
11
|
+
"https://api.x.com/2/oauth2/token",
|
|
12
|
+
{
|
|
13
|
+
code: "auth-code",
|
|
14
|
+
codeVerifier: "verifier",
|
|
15
|
+
clientId: "client-id",
|
|
16
|
+
clientSecret: "client-secret",
|
|
17
|
+
redirectUri: "https://example.com/callback",
|
|
18
|
+
clientAuthMethod: "client_secret_basic",
|
|
19
|
+
},
|
|
20
|
+
fetch,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
24
|
+
const [, init] = fetch.mock.calls[0] as [string, RequestInit];
|
|
25
|
+
const headers = init.headers as Record<string, string>;
|
|
26
|
+
expect(headers.Authorization).toBe(
|
|
27
|
+
`Basic ${Buffer.from("client-id:client-secret").toString("base64")}`,
|
|
28
|
+
);
|
|
29
|
+
expect(String(init.body)).not.toContain("client_secret");
|
|
30
|
+
expect(String(init.body)).not.toContain("client_id");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses client_secret_post by default", async () => {
|
|
34
|
+
const fetch = vi.fn(async () => Response.json({ access_token: "at" }));
|
|
35
|
+
|
|
36
|
+
await exchangeCodeForTokens(
|
|
37
|
+
"https://example.com/token",
|
|
38
|
+
{
|
|
39
|
+
code: "auth-code",
|
|
40
|
+
codeVerifier: "verifier",
|
|
41
|
+
clientId: "client-id",
|
|
42
|
+
clientSecret: "client-secret",
|
|
43
|
+
redirectUri: "https://example.com/callback",
|
|
44
|
+
},
|
|
45
|
+
fetch,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const [, init] = fetch.mock.calls[0] as [string, RequestInit];
|
|
49
|
+
const headers = init.headers as Record<string, string>;
|
|
50
|
+
expect(headers.Authorization).toBeUndefined();
|
|
51
|
+
expect(String(init.body)).toContain("client_secret=client-secret");
|
|
52
|
+
expect(String(init.body)).toContain("client_id=client-id");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("refreshAccessToken", () => {
|
|
57
|
+
it("uses HTTP Basic auth for client_secret_basic", async () => {
|
|
58
|
+
const fetch = vi.fn(async () => Response.json({ access_token: "at" }));
|
|
59
|
+
|
|
60
|
+
await refreshAccessToken(
|
|
61
|
+
"https://api.x.com/2/oauth2/token",
|
|
62
|
+
{
|
|
63
|
+
refreshToken: "refresh-token",
|
|
64
|
+
clientId: "client-id",
|
|
65
|
+
clientSecret: "client-secret",
|
|
66
|
+
clientAuthMethod: "client_secret_basic",
|
|
67
|
+
},
|
|
68
|
+
fetch,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const [, init] = fetch.mock.calls[0] as [string, RequestInit];
|
|
72
|
+
const headers = init.headers as Record<string, string>;
|
|
73
|
+
expect(headers.Authorization).toBe(
|
|
74
|
+
`Basic ${Buffer.from("client-id:client-secret").toString("base64")}`,
|
|
75
|
+
);
|
|
76
|
+
expect(String(init.body)).toBe(
|
|
77
|
+
"grant_type=refresh_token&refresh_token=refresh-token",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/mcp-client.ts
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* by registry-consumer — this module only provides auth primitives.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { generatePkcePair } from "./pkce.js";
|
|
17
16
|
import type { RegistryAuthRequirement } from "./define-config.js";
|
|
18
17
|
import type { FetchFn } from "./fetch-types.js";
|
|
18
|
+
import { generatePkcePair } from "./pkce.js";
|
|
19
19
|
|
|
20
20
|
// ============================================
|
|
21
21
|
// Types
|
|
@@ -82,8 +82,7 @@ export async function dynamicClientRegistration(
|
|
|
82
82
|
client_name: params.clientName,
|
|
83
83
|
redirect_uris: params.redirectUris,
|
|
84
84
|
grant_types: params.grantTypes ?? ["authorization_code"],
|
|
85
|
-
token_endpoint_auth_method:
|
|
86
|
-
params.tokenEndpointAuthMethod ?? "none",
|
|
85
|
+
token_endpoint_auth_method: params.tokenEndpointAuthMethod ?? "none",
|
|
87
86
|
}),
|
|
88
87
|
});
|
|
89
88
|
if (!res.ok) {
|
|
@@ -148,6 +147,69 @@ export async function buildOAuthAuthorizeUrl(params: {
|
|
|
148
147
|
// Token Exchange
|
|
149
148
|
// ============================================
|
|
150
149
|
|
|
150
|
+
export type OAuthClientAuthMethod =
|
|
151
|
+
| "client_secret_post"
|
|
152
|
+
| "client_secret_basic";
|
|
153
|
+
|
|
154
|
+
const TOKEN_EXCHANGE_BODY_PARAMS: Record<OAuthClientAuthMethod, string[]> = {
|
|
155
|
+
client_secret_post: [
|
|
156
|
+
"grant_type",
|
|
157
|
+
"code",
|
|
158
|
+
"code_verifier",
|
|
159
|
+
"redirect_uri",
|
|
160
|
+
"client_id",
|
|
161
|
+
"client_secret",
|
|
162
|
+
],
|
|
163
|
+
client_secret_basic: ["grant_type", "code", "code_verifier", "redirect_uri"],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const TOKEN_REFRESH_BODY_PARAMS: Record<OAuthClientAuthMethod, string[]> = {
|
|
167
|
+
client_secret_post: [
|
|
168
|
+
"grant_type",
|
|
169
|
+
"refresh_token",
|
|
170
|
+
"client_id",
|
|
171
|
+
"client_secret",
|
|
172
|
+
],
|
|
173
|
+
client_secret_basic: ["grant_type", "refresh_token"],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function buildBasicAuth(clientId: string, clientSecret: string): string {
|
|
177
|
+
return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildOAuthTokenRequest(params: {
|
|
181
|
+
allParams: Record<string, string>;
|
|
182
|
+
bodyParamKeys: string[];
|
|
183
|
+
clientId: string;
|
|
184
|
+
clientSecret?: string;
|
|
185
|
+
clientAuthMethod: OAuthClientAuthMethod;
|
|
186
|
+
}): { headers: Record<string, string>; body: string } {
|
|
187
|
+
const body = new URLSearchParams();
|
|
188
|
+
for (const key of params.bodyParamKeys) {
|
|
189
|
+
const value = params.allParams[key];
|
|
190
|
+
if (value) body.set(key, value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const headers: Record<string, string> = {
|
|
194
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
195
|
+
Accept: "application/json",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
params.clientAuthMethod === "client_secret_basic" &&
|
|
200
|
+
params.clientSecret
|
|
201
|
+
) {
|
|
202
|
+
headers.Authorization = buildBasicAuth(
|
|
203
|
+
params.clientId,
|
|
204
|
+
params.clientSecret,
|
|
205
|
+
);
|
|
206
|
+
} else if (params.clientSecret) {
|
|
207
|
+
body.set("client_secret", params.clientSecret);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { headers, body: body.toString() };
|
|
211
|
+
}
|
|
212
|
+
|
|
151
213
|
/**
|
|
152
214
|
* Exchange an authorization code for tokens (with PKCE).
|
|
153
215
|
*/
|
|
@@ -159,29 +221,35 @@ export async function exchangeCodeForTokens(
|
|
|
159
221
|
clientId: string;
|
|
160
222
|
clientSecret?: string;
|
|
161
223
|
redirectUri: string;
|
|
224
|
+
clientAuthMethod?: OAuthClientAuthMethod;
|
|
162
225
|
},
|
|
163
|
-
fetchFn:
|
|
226
|
+
fetchFn: FetchFn = globalThis.fetch,
|
|
164
227
|
): Promise<{
|
|
165
228
|
accessToken: string;
|
|
166
229
|
refreshToken?: string;
|
|
167
230
|
expiresIn?: number;
|
|
168
231
|
tokenType?: string;
|
|
169
232
|
}> {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
233
|
+
const method = params.clientAuthMethod ?? "client_secret_post";
|
|
234
|
+
const { headers, body } = buildOAuthTokenRequest({
|
|
235
|
+
allParams: {
|
|
236
|
+
grant_type: "authorization_code",
|
|
237
|
+
code: params.code,
|
|
238
|
+
code_verifier: params.codeVerifier,
|
|
239
|
+
redirect_uri: params.redirectUri,
|
|
240
|
+
client_id: params.clientId,
|
|
241
|
+
...(params.clientSecret && { client_secret: params.clientSecret }),
|
|
242
|
+
},
|
|
243
|
+
bodyParamKeys: TOKEN_EXCHANGE_BODY_PARAMS[method],
|
|
244
|
+
clientId: params.clientId,
|
|
245
|
+
clientSecret: params.clientSecret,
|
|
246
|
+
clientAuthMethod: method,
|
|
176
247
|
});
|
|
177
|
-
if (params.clientSecret) {
|
|
178
|
-
body.set("client_secret", params.clientSecret);
|
|
179
|
-
}
|
|
180
248
|
|
|
181
249
|
const res = await fetchFn(tokenEndpoint, {
|
|
182
250
|
method: "POST",
|
|
183
|
-
headers
|
|
184
|
-
body
|
|
251
|
+
headers,
|
|
252
|
+
body,
|
|
185
253
|
});
|
|
186
254
|
if (!res.ok) {
|
|
187
255
|
const text = await res.text().catch(() => "unknown");
|
|
@@ -209,26 +277,32 @@ export async function refreshAccessToken(
|
|
|
209
277
|
refreshToken: string;
|
|
210
278
|
clientId: string;
|
|
211
279
|
clientSecret?: string;
|
|
280
|
+
clientAuthMethod?: OAuthClientAuthMethod;
|
|
212
281
|
},
|
|
213
|
-
fetchFn:
|
|
282
|
+
fetchFn: FetchFn = globalThis.fetch,
|
|
214
283
|
): Promise<{
|
|
215
284
|
accessToken: string;
|
|
216
285
|
refreshToken?: string;
|
|
217
286
|
expiresIn?: number;
|
|
218
287
|
}> {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
288
|
+
const method = params.clientAuthMethod ?? "client_secret_post";
|
|
289
|
+
const { headers, body } = buildOAuthTokenRequest({
|
|
290
|
+
allParams: {
|
|
291
|
+
grant_type: "refresh_token",
|
|
292
|
+
refresh_token: params.refreshToken,
|
|
293
|
+
client_id: params.clientId,
|
|
294
|
+
...(params.clientSecret && { client_secret: params.clientSecret }),
|
|
295
|
+
},
|
|
296
|
+
bodyParamKeys: TOKEN_REFRESH_BODY_PARAMS[method],
|
|
297
|
+
clientId: params.clientId,
|
|
298
|
+
clientSecret: params.clientSecret,
|
|
299
|
+
clientAuthMethod: method,
|
|
223
300
|
});
|
|
224
|
-
if (params.clientSecret) {
|
|
225
|
-
body.set("client_secret", params.clientSecret);
|
|
226
|
-
}
|
|
227
301
|
|
|
228
302
|
const res = await fetchFn(tokenEndpoint, {
|
|
229
303
|
method: "POST",
|
|
230
|
-
headers
|
|
231
|
-
body
|
|
304
|
+
headers,
|
|
305
|
+
body,
|
|
232
306
|
});
|
|
233
307
|
if (!res.ok) {
|
|
234
308
|
const text = await res.text().catch(() => "unknown");
|
|
@@ -252,14 +326,17 @@ export async function refreshAccessToken(
|
|
|
252
326
|
* Returns the scheme and any `key="value"` params. Tolerant of
|
|
253
327
|
* single-value headers and missing params.
|
|
254
328
|
*/
|
|
255
|
-
export function parseWwwAuthenticate(
|
|
256
|
-
|
|
257
|
-
|
|
329
|
+
export function parseWwwAuthenticate(header: string): {
|
|
330
|
+
scheme: string;
|
|
331
|
+
params: Record<string, string>;
|
|
332
|
+
} {
|
|
258
333
|
const spaceIdx = header.indexOf(" ");
|
|
259
334
|
const scheme = (spaceIdx === -1 ? header : header.slice(0, spaceIdx)).trim();
|
|
260
335
|
const rest = spaceIdx === -1 ? "" : header.slice(spaceIdx + 1);
|
|
261
336
|
const params: Record<string, string> = {};
|
|
262
|
-
for (const match of rest.matchAll(
|
|
337
|
+
for (const match of rest.matchAll(
|
|
338
|
+
/([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/g,
|
|
339
|
+
)) {
|
|
263
340
|
params[match[1]!.toLowerCase()] = match[2]!;
|
|
264
341
|
}
|
|
265
342
|
return { scheme, params };
|
|
@@ -315,7 +392,10 @@ export async function probeRegistryAuth(
|
|
|
315
392
|
try {
|
|
316
393
|
res = await fetchFn(url, {
|
|
317
394
|
method: "POST",
|
|
318
|
-
headers: {
|
|
395
|
+
headers: {
|
|
396
|
+
"Content-Type": "application/json",
|
|
397
|
+
Accept: "application/json",
|
|
398
|
+
},
|
|
319
399
|
body: JSON.stringify({
|
|
320
400
|
jsonrpc: "2.0",
|
|
321
401
|
id: 1,
|
|
@@ -344,7 +424,10 @@ export async function probeRegistryAuth(
|
|
|
344
424
|
const metadataUrl = params.resource_metadata;
|
|
345
425
|
if (metadataUrl) {
|
|
346
426
|
requirement.resourceMetadataUrl = metadataUrl;
|
|
347
|
-
const metadata = await discoverProtectedResourceMetadata(
|
|
427
|
+
const metadata = await discoverProtectedResourceMetadata(
|
|
428
|
+
metadataUrl,
|
|
429
|
+
fetchFn,
|
|
430
|
+
);
|
|
348
431
|
if (metadata) {
|
|
349
432
|
if (metadata.authorization_servers?.length) {
|
|
350
433
|
requirement.authorizationServers = metadata.authorization_servers;
|