@toolsdk.ai/registry 1.0.132 → 1.0.133
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/api/index.js +4 -0
- package/dist/domains/executor/executor-types.d.ts +3 -1
- package/dist/domains/executor/local-executor.d.ts +1 -1
- package/dist/domains/executor/local-executor.js +3 -3
- package/dist/domains/executor/sandbox-executor.d.ts +1 -1
- package/dist/domains/executor/sandbox-executor.js +3 -3
- package/dist/domains/oauth/__tests__/oauth-session.test.d.ts +1 -0
- package/dist/domains/oauth/__tests__/oauth-session.test.js +272 -0
- package/dist/domains/oauth/__tests__/oauth-utils.test.d.ts +1 -0
- package/dist/domains/oauth/__tests__/oauth-utils.test.js +284 -0
- package/dist/domains/oauth/index.d.ts +9 -0
- package/dist/domains/oauth/index.js +9 -0
- package/dist/domains/oauth/oauth-handler.d.ts +65 -0
- package/dist/domains/oauth/oauth-handler.js +355 -0
- package/dist/domains/oauth/oauth-route.d.ts +11 -0
- package/dist/domains/oauth/oauth-route.js +138 -0
- package/dist/domains/oauth/oauth-schema.d.ts +257 -0
- package/dist/domains/oauth/oauth-schema.js +119 -0
- package/dist/domains/oauth/oauth-session.d.ts +54 -0
- package/dist/domains/oauth/oauth-session.js +116 -0
- package/dist/domains/oauth/oauth-types.d.ts +148 -0
- package/dist/domains/oauth/oauth-types.js +9 -0
- package/dist/domains/oauth/oauth-utils.d.ts +99 -0
- package/dist/domains/oauth/oauth-utils.js +267 -0
- package/dist/domains/package/package-handler.d.ts +2 -2
- package/dist/domains/package/package-handler.js +4 -4
- package/dist/domains/package/package-route.js +5 -5
- package/dist/domains/package/package-schema.d.ts +51 -0
- package/dist/domains/package/package-schema.js +17 -0
- package/dist/domains/package/package-so.d.ts +6 -2
- package/dist/domains/package/package-so.js +4 -3
- package/dist/shared/schemas/common-schema.d.ts +52 -0
- package/dist/shared/schemas/common-schema.js +7 -0
- package/dist/shared/scripts-helpers/index.d.ts +4 -0
- package/dist/shared/utils/mcp-client-util.d.ts +1 -1
- package/dist/shared/utils/mcp-client-util.js +13 -4
- package/package.json +1 -1
- package/packages/developer-tools/github-mcp.json +4 -1
package/dist/api/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { serve } from "@hono/node-server";
|
|
|
4
4
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
5
5
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
6
6
|
import { configRoutes } from "../domains/config/config-route";
|
|
7
|
+
import { oauthDemoRoutes, oauthRoutes } from "../domains/oauth/oauth-route";
|
|
7
8
|
import { repository } from "../domains/package/package-handler";
|
|
8
9
|
import { packageRoutes } from "../domains/package/package-route";
|
|
9
10
|
import { initRegistryFactory } from "../domains/registry/registry-factory";
|
|
@@ -31,6 +32,9 @@ const app = new OpenAPIHono();
|
|
|
31
32
|
// Domain routes
|
|
32
33
|
app.route("/api/v1", packageRoutes);
|
|
33
34
|
app.route("/api/v1/config", configRoutes);
|
|
35
|
+
app.route("/api/v1/oauth", oauthRoutes);
|
|
36
|
+
// Demo routes (serves demo-oauth.html and handles callbacks)
|
|
37
|
+
app.route("/demo", oauthDemoRoutes);
|
|
34
38
|
if (isSearchEnabled()) {
|
|
35
39
|
initializeSearchService().catch(console.error);
|
|
36
40
|
app.route("/api/v1/search", searchRoutes);
|
|
@@ -6,6 +6,8 @@ export interface ToolExecuteRequest {
|
|
|
6
6
|
inputData: Record<string, unknown>;
|
|
7
7
|
envs?: Record<string, string>;
|
|
8
8
|
sandboxProvider?: MCPSandboxProvider;
|
|
9
|
+
/** OAuth access token for MCP servers that require OAuth authentication */
|
|
10
|
+
accessToken?: string;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Tool Executor Interface
|
|
@@ -13,5 +15,5 @@ export interface ToolExecuteRequest {
|
|
|
13
15
|
*/
|
|
14
16
|
export interface ToolExecutor {
|
|
15
17
|
executeTool(request: ToolExecuteRequest): Promise<unknown>;
|
|
16
|
-
listTools(packageName: string,
|
|
18
|
+
listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
|
|
17
19
|
}
|
|
@@ -8,5 +8,5 @@ export declare class LocalExecutor implements ToolExecutor {
|
|
|
8
8
|
private readonly packageRepository;
|
|
9
9
|
constructor();
|
|
10
10
|
executeTool(request: ToolExecuteRequest): Promise<unknown>;
|
|
11
|
-
listTools(packageName: string): Promise<Tool[]>;
|
|
11
|
+
listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
|
|
12
12
|
}
|
|
@@ -14,7 +14,7 @@ export class LocalExecutor {
|
|
|
14
14
|
}
|
|
15
15
|
async executeTool(request) {
|
|
16
16
|
const mcpServerConfig = this.packageRepository.getPackageConfig(request.packageName);
|
|
17
|
-
const { client, closeConnection } = await getMcpClient(mcpServerConfig, request.envs || {});
|
|
17
|
+
const { client, closeConnection } = await getMcpClient(mcpServerConfig, request.envs || {}, request.accessToken);
|
|
18
18
|
try {
|
|
19
19
|
const result = await client.callTool({
|
|
20
20
|
name: request.toolKey,
|
|
@@ -27,7 +27,7 @@ export class LocalExecutor {
|
|
|
27
27
|
await closeConnection();
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async listTools(packageName) {
|
|
30
|
+
async listTools(packageName, accessToken) {
|
|
31
31
|
const mcpServerConfig = this.packageRepository.getPackageConfig(packageName);
|
|
32
32
|
const mockEnvs = {};
|
|
33
33
|
if (mcpServerConfig.env) {
|
|
@@ -35,7 +35,7 @@ export class LocalExecutor {
|
|
|
35
35
|
mockEnvs[key] = "mock_value";
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
const { client, closeConnection } = await getMcpClient(mcpServerConfig, mockEnvs);
|
|
38
|
+
const { client, closeConnection } = await getMcpClient(mcpServerConfig, mockEnvs, accessToken);
|
|
39
39
|
try {
|
|
40
40
|
const { tools } = await client.listTools();
|
|
41
41
|
console.log(`[LocalExecutor] Tools list retrieved successfully for package ${packageName}`);
|
|
@@ -12,5 +12,5 @@ export declare class SandboxExecutor implements ToolExecutor {
|
|
|
12
12
|
private readonly localExecutor;
|
|
13
13
|
constructor(provider: MCPSandboxProvider);
|
|
14
14
|
executeTool(request: ToolExecuteRequest): Promise<unknown>;
|
|
15
|
-
listTools(packageName: string): Promise<Tool[]>;
|
|
15
|
+
listTools(packageName: string, accessToken?: string): Promise<Tool[]>;
|
|
16
16
|
}
|
|
@@ -48,13 +48,13 @@ export class SandboxExecutor {
|
|
|
48
48
|
await this.sandboxPool.release(runtime, this.provider);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
async listTools(packageName) {
|
|
51
|
+
async listTools(packageName, accessToken) {
|
|
52
52
|
const mcpServerConfig = this.packageRepository.getPackageConfig(packageName);
|
|
53
53
|
const runtime = mcpServerConfig.runtime || "python";
|
|
54
54
|
// Sandbox only supports node runtime, fallback to LOCAL for other runtimes
|
|
55
55
|
if (runtime !== "node") {
|
|
56
56
|
console.log(`[SandboxExecutor] Runtime '${runtime}' is not supported in sandbox, using LOCAL execution`);
|
|
57
|
-
return await this.localExecutor.listTools(packageName);
|
|
57
|
+
return await this.localExecutor.listTools(packageName, accessToken);
|
|
58
58
|
}
|
|
59
59
|
const sandboxClient = await this.sandboxPool.acquire(runtime, this.provider);
|
|
60
60
|
try {
|
|
@@ -67,7 +67,7 @@ export class SandboxExecutor {
|
|
|
67
67
|
console.warn(`[SandboxExecutor] sandbox list tools failed, falling back to LOCAL execution`);
|
|
68
68
|
console.warn(`[SandboxExecutor] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
69
|
try {
|
|
70
|
-
const tools = await this.localExecutor.listTools(packageName);
|
|
70
|
+
const tools = await this.localExecutor.listTools(packageName, accessToken);
|
|
71
71
|
console.log(`[SandboxExecutor] Tools list retrieved successfully with LOCAL fallback`);
|
|
72
72
|
return tools;
|
|
73
73
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { OAuthSessionStore } from "../oauth-session";
|
|
3
|
+
// Create a new instance for testing (not the singleton)
|
|
4
|
+
function createTestStore() {
|
|
5
|
+
const store = new OAuthSessionStore();
|
|
6
|
+
// Stop the cleanup timer to avoid interference with tests
|
|
7
|
+
store.stopCleanup();
|
|
8
|
+
return store;
|
|
9
|
+
}
|
|
10
|
+
function createMockSession(overrides = {}) {
|
|
11
|
+
return Object.assign({ sessionId: "test-session-id", state: "test-state", codeVerifier: "test-code-verifier", codeChallenge: "test-code-challenge", clientInfo: {
|
|
12
|
+
client_id: "test-client-id",
|
|
13
|
+
}, callbackBaseUrl: "http://localhost:3003/callback", mcpServerUrl: "http://localhost:3001/mcp", packageName: "github-mcp", oauthMetadata: {
|
|
14
|
+
issuer: "http://localhost:3001",
|
|
15
|
+
authorization_endpoint: "http://localhost:3001/authorize",
|
|
16
|
+
token_endpoint: "http://localhost:3001/token",
|
|
17
|
+
}, createdAt: Date.now() }, overrides);
|
|
18
|
+
}
|
|
19
|
+
describe("OAuthSessionStore", () => {
|
|
20
|
+
let store;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
store = createTestStore();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
store.stopCleanup();
|
|
26
|
+
store.clear();
|
|
27
|
+
});
|
|
28
|
+
describe("set", () => {
|
|
29
|
+
it("should store a session", () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const session = createMockSession();
|
|
32
|
+
// Act
|
|
33
|
+
store.set(session);
|
|
34
|
+
// Assert
|
|
35
|
+
expect(store.has(session.sessionId)).toBe(true);
|
|
36
|
+
expect(store.size()).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it("should allow retrieval by sessionId after set", () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const session = createMockSession();
|
|
41
|
+
// Act
|
|
42
|
+
store.set(session);
|
|
43
|
+
const retrieved = store.get(session.sessionId);
|
|
44
|
+
// Assert
|
|
45
|
+
expect(retrieved).toEqual(session);
|
|
46
|
+
});
|
|
47
|
+
it("should allow retrieval by state after set", () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const session = createMockSession();
|
|
50
|
+
// Act
|
|
51
|
+
store.set(session);
|
|
52
|
+
const retrieved = store.getByState(session.state);
|
|
53
|
+
// Assert
|
|
54
|
+
expect(retrieved).toEqual(session);
|
|
55
|
+
});
|
|
56
|
+
it("should overwrite session with same sessionId", () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const session1 = createMockSession({ packageName: "package1" });
|
|
59
|
+
const session2 = createMockSession({ packageName: "package2" });
|
|
60
|
+
// Act
|
|
61
|
+
store.set(session1);
|
|
62
|
+
store.set(session2);
|
|
63
|
+
// Assert
|
|
64
|
+
const retrieved = store.get(session1.sessionId);
|
|
65
|
+
expect(retrieved === null || retrieved === void 0 ? void 0 : retrieved.packageName).toBe("package2");
|
|
66
|
+
expect(store.size()).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("get", () => {
|
|
70
|
+
it("should return undefined for non-existent session", () => {
|
|
71
|
+
// Act
|
|
72
|
+
const result = store.get("non-existent-id");
|
|
73
|
+
// Assert
|
|
74
|
+
expect(result).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it("should return session when exists", () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const session = createMockSession();
|
|
79
|
+
store.set(session);
|
|
80
|
+
// Act
|
|
81
|
+
const result = store.get(session.sessionId);
|
|
82
|
+
// Assert
|
|
83
|
+
expect(result).toEqual(session);
|
|
84
|
+
});
|
|
85
|
+
it("should return undefined for expired session", () => {
|
|
86
|
+
// Arrange - create session that expired 11 minutes ago
|
|
87
|
+
const expiredSession = createMockSession({
|
|
88
|
+
sessionId: "expired-session",
|
|
89
|
+
createdAt: Date.now() - 11 * 60 * 1000,
|
|
90
|
+
});
|
|
91
|
+
store.set(expiredSession);
|
|
92
|
+
// Act
|
|
93
|
+
const result = store.get("expired-session");
|
|
94
|
+
// Assert
|
|
95
|
+
expect(result).toBeUndefined();
|
|
96
|
+
expect(store.has("expired-session")).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it("should return session that is still valid (9 minutes old)", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const validSession = createMockSession({
|
|
101
|
+
sessionId: "valid-session",
|
|
102
|
+
createdAt: Date.now() - 9 * 60 * 1000,
|
|
103
|
+
});
|
|
104
|
+
store.set(validSession);
|
|
105
|
+
// Act
|
|
106
|
+
const result = store.get("valid-session");
|
|
107
|
+
// Assert
|
|
108
|
+
expect(result).toBeDefined();
|
|
109
|
+
expect(result === null || result === void 0 ? void 0 : result.sessionId).toBe("valid-session");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("getByState", () => {
|
|
113
|
+
it("should return undefined for non-existent state", () => {
|
|
114
|
+
// Act
|
|
115
|
+
const result = store.getByState("non-existent-state");
|
|
116
|
+
// Assert
|
|
117
|
+
expect(result).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
it("should return session when state exists", () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const session = createMockSession({ state: "unique-state" });
|
|
122
|
+
store.set(session);
|
|
123
|
+
// Act
|
|
124
|
+
const result = store.getByState("unique-state");
|
|
125
|
+
// Assert
|
|
126
|
+
expect(result).toEqual(session);
|
|
127
|
+
});
|
|
128
|
+
it("should return undefined for expired session by state", () => {
|
|
129
|
+
// Arrange
|
|
130
|
+
const expiredSession = createMockSession({
|
|
131
|
+
state: "expired-state",
|
|
132
|
+
createdAt: Date.now() - 11 * 60 * 1000,
|
|
133
|
+
});
|
|
134
|
+
store.set(expiredSession);
|
|
135
|
+
// Act
|
|
136
|
+
const result = store.getByState("expired-state");
|
|
137
|
+
// Assert
|
|
138
|
+
expect(result).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("delete", () => {
|
|
142
|
+
it("should return false for non-existent session", () => {
|
|
143
|
+
// Act
|
|
144
|
+
const result = store.delete("non-existent-id");
|
|
145
|
+
// Assert
|
|
146
|
+
expect(result).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it("should delete existing session and return true", () => {
|
|
149
|
+
// Arrange
|
|
150
|
+
const session = createMockSession();
|
|
151
|
+
store.set(session);
|
|
152
|
+
// Act
|
|
153
|
+
const result = store.delete(session.sessionId);
|
|
154
|
+
// Assert
|
|
155
|
+
expect(result).toBe(true);
|
|
156
|
+
expect(store.has(session.sessionId)).toBe(false);
|
|
157
|
+
expect(store.size()).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
it("should also remove state mapping when session is deleted", () => {
|
|
160
|
+
// Arrange
|
|
161
|
+
const session = createMockSession({ state: "state-to-delete" });
|
|
162
|
+
store.set(session);
|
|
163
|
+
// Act
|
|
164
|
+
store.delete(session.sessionId);
|
|
165
|
+
const byState = store.getByState("state-to-delete");
|
|
166
|
+
// Assert
|
|
167
|
+
expect(byState).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("has", () => {
|
|
171
|
+
it("should return false for non-existent session", () => {
|
|
172
|
+
// Act
|
|
173
|
+
const result = store.has("non-existent-id");
|
|
174
|
+
// Assert
|
|
175
|
+
expect(result).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
it("should return true for existing session", () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
const session = createMockSession();
|
|
180
|
+
store.set(session);
|
|
181
|
+
// Act
|
|
182
|
+
const result = store.has(session.sessionId);
|
|
183
|
+
// Assert
|
|
184
|
+
expect(result).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("size", () => {
|
|
188
|
+
it("should return 0 for empty store", () => {
|
|
189
|
+
// Act
|
|
190
|
+
const result = store.size();
|
|
191
|
+
// Assert
|
|
192
|
+
expect(result).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
it("should return correct count after adding sessions", () => {
|
|
195
|
+
// Arrange
|
|
196
|
+
store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
|
|
197
|
+
store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
|
|
198
|
+
store.set(createMockSession({ sessionId: "session-3", state: "state-3" }));
|
|
199
|
+
// Act
|
|
200
|
+
const result = store.size();
|
|
201
|
+
// Assert
|
|
202
|
+
expect(result).toBe(3);
|
|
203
|
+
});
|
|
204
|
+
it("should return correct count after deleting sessions", () => {
|
|
205
|
+
// Arrange
|
|
206
|
+
store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
|
|
207
|
+
store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
|
|
208
|
+
store.delete("session-1");
|
|
209
|
+
// Act
|
|
210
|
+
const result = store.size();
|
|
211
|
+
// Assert
|
|
212
|
+
expect(result).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe("clear", () => {
|
|
216
|
+
it("should remove all sessions", () => {
|
|
217
|
+
// Arrange
|
|
218
|
+
store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
|
|
219
|
+
store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
|
|
220
|
+
// Act
|
|
221
|
+
store.clear();
|
|
222
|
+
// Assert
|
|
223
|
+
expect(store.size()).toBe(0);
|
|
224
|
+
expect(store.has("session-1")).toBe(false);
|
|
225
|
+
expect(store.has("session-2")).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
it("should also clear state mappings", () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
|
|
230
|
+
// Act
|
|
231
|
+
store.clear();
|
|
232
|
+
const byState = store.getByState("state-1");
|
|
233
|
+
// Assert
|
|
234
|
+
expect(byState).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("multiple sessions", () => {
|
|
238
|
+
it("should handle multiple sessions with different states", () => {
|
|
239
|
+
var _a, _b, _c, _d;
|
|
240
|
+
// Arrange
|
|
241
|
+
const session1 = createMockSession({
|
|
242
|
+
sessionId: "session-1",
|
|
243
|
+
state: "state-1",
|
|
244
|
+
packageName: "package-1",
|
|
245
|
+
});
|
|
246
|
+
const session2 = createMockSession({
|
|
247
|
+
sessionId: "session-2",
|
|
248
|
+
state: "state-2",
|
|
249
|
+
packageName: "package-2",
|
|
250
|
+
});
|
|
251
|
+
// Act
|
|
252
|
+
store.set(session1);
|
|
253
|
+
store.set(session2);
|
|
254
|
+
// Assert
|
|
255
|
+
expect((_a = store.get("session-1")) === null || _a === void 0 ? void 0 : _a.packageName).toBe("package-1");
|
|
256
|
+
expect((_b = store.get("session-2")) === null || _b === void 0 ? void 0 : _b.packageName).toBe("package-2");
|
|
257
|
+
expect((_c = store.getByState("state-1")) === null || _c === void 0 ? void 0 : _c.packageName).toBe("package-1");
|
|
258
|
+
expect((_d = store.getByState("state-2")) === null || _d === void 0 ? void 0 : _d.packageName).toBe("package-2");
|
|
259
|
+
});
|
|
260
|
+
it("should not affect other sessions when deleting one", () => {
|
|
261
|
+
// Arrange
|
|
262
|
+
store.set(createMockSession({ sessionId: "session-1", state: "state-1" }));
|
|
263
|
+
store.set(createMockSession({ sessionId: "session-2", state: "state-2" }));
|
|
264
|
+
// Act
|
|
265
|
+
store.delete("session-1");
|
|
266
|
+
// Assert
|
|
267
|
+
expect(store.has("session-1")).toBe(false);
|
|
268
|
+
expect(store.has("session-2")).toBe(true);
|
|
269
|
+
expect(store.getByState("state-2")).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildAuthorizationUrl, generatePKCE, generateSessionId, generateState, getCanonicalResourceUri, parseWWWAuthenticate, verifyPKCESupport, } from "../oauth-utils";
|
|
3
|
+
describe("oauth-utils", () => {
|
|
4
|
+
describe("generatePKCE", () => {
|
|
5
|
+
it("should generate valid PKCE parameters", () => {
|
|
6
|
+
// Act
|
|
7
|
+
const pkce = generatePKCE();
|
|
8
|
+
// Assert
|
|
9
|
+
expect(pkce).toHaveProperty("codeVerifier");
|
|
10
|
+
expect(pkce).toHaveProperty("codeChallenge");
|
|
11
|
+
expect(pkce).toHaveProperty("codeChallengeMethod");
|
|
12
|
+
expect(pkce.codeChallengeMethod).toBe("S256");
|
|
13
|
+
});
|
|
14
|
+
it("should generate code verifier with correct length (43 chars base64url)", () => {
|
|
15
|
+
// Act
|
|
16
|
+
const pkce = generatePKCE();
|
|
17
|
+
// Assert - 32 bytes base64url encoded = 43 characters
|
|
18
|
+
expect(pkce.codeVerifier.length).toBe(43);
|
|
19
|
+
});
|
|
20
|
+
it("should generate code challenge with correct length", () => {
|
|
21
|
+
// Act
|
|
22
|
+
const pkce = generatePKCE();
|
|
23
|
+
// Assert - SHA256 hash (32 bytes) base64url encoded = 43 characters
|
|
24
|
+
expect(pkce.codeChallenge.length).toBe(43);
|
|
25
|
+
});
|
|
26
|
+
it("should generate different values each time", () => {
|
|
27
|
+
// Act
|
|
28
|
+
const pkce1 = generatePKCE();
|
|
29
|
+
const pkce2 = generatePKCE();
|
|
30
|
+
// Assert
|
|
31
|
+
expect(pkce1.codeVerifier).not.toBe(pkce2.codeVerifier);
|
|
32
|
+
expect(pkce1.codeChallenge).not.toBe(pkce2.codeChallenge);
|
|
33
|
+
});
|
|
34
|
+
it("should generate base64url encoded values (no +, /, =)", () => {
|
|
35
|
+
// Act
|
|
36
|
+
const pkce = generatePKCE();
|
|
37
|
+
// Assert - base64url should not contain +, /, or =
|
|
38
|
+
expect(pkce.codeVerifier).not.toMatch(/[+/=]/);
|
|
39
|
+
expect(pkce.codeChallenge).not.toMatch(/[+/=]/);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("generateState", () => {
|
|
43
|
+
it("should generate a valid UUID", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const state = generateState();
|
|
46
|
+
// Assert - UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
47
|
+
expect(state).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
48
|
+
});
|
|
49
|
+
it("should generate different values each time", () => {
|
|
50
|
+
// Act
|
|
51
|
+
const state1 = generateState();
|
|
52
|
+
const state2 = generateState();
|
|
53
|
+
// Assert
|
|
54
|
+
expect(state1).not.toBe(state2);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("generateSessionId", () => {
|
|
58
|
+
it("should generate a valid UUID", () => {
|
|
59
|
+
// Act
|
|
60
|
+
const sessionId = generateSessionId();
|
|
61
|
+
// Assert
|
|
62
|
+
expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
63
|
+
});
|
|
64
|
+
it("should generate different values each time", () => {
|
|
65
|
+
// Act
|
|
66
|
+
const id1 = generateSessionId();
|
|
67
|
+
const id2 = generateSessionId();
|
|
68
|
+
// Assert
|
|
69
|
+
expect(id1).not.toBe(id2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("parseWWWAuthenticate", () => {
|
|
73
|
+
it("should parse complete WWW-Authenticate header", () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const header = 'Bearer realm="mcp", resource_metadata="http://localhost:3001/.well-known/oauth-protected-resource", scope="read write"';
|
|
76
|
+
// Act
|
|
77
|
+
const result = parseWWWAuthenticate(header);
|
|
78
|
+
// Assert
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
realm: "mcp",
|
|
81
|
+
resourceMetadataUrl: "http://localhost:3001/.well-known/oauth-protected-resource",
|
|
82
|
+
scope: "read write",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it("should parse header with only realm", () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const header = 'Bearer realm="example"';
|
|
88
|
+
// Act
|
|
89
|
+
const result = parseWWWAuthenticate(header);
|
|
90
|
+
// Assert
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
realm: "example",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
it("should parse header with only resource_metadata", () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const header = 'Bearer resource_metadata="http://example.com/.well-known/oauth-protected-resource"';
|
|
98
|
+
// Act
|
|
99
|
+
const result = parseWWWAuthenticate(header);
|
|
100
|
+
// Assert
|
|
101
|
+
expect(result).toEqual({
|
|
102
|
+
resourceMetadataUrl: "http://example.com/.well-known/oauth-protected-resource",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it("should return empty object for empty header", () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
const header = "Bearer";
|
|
108
|
+
// Act
|
|
109
|
+
const result = parseWWWAuthenticate(header);
|
|
110
|
+
// Assert
|
|
111
|
+
expect(result).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("verifyPKCESupport", () => {
|
|
115
|
+
it("should return supported=true when S256 is in supported methods", () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const metadata = {
|
|
118
|
+
issuer: "http://localhost:3001",
|
|
119
|
+
authorization_endpoint: "http://localhost:3001/authorize",
|
|
120
|
+
token_endpoint: "http://localhost:3001/token",
|
|
121
|
+
code_challenge_methods_supported: ["S256", "plain"],
|
|
122
|
+
};
|
|
123
|
+
// Act
|
|
124
|
+
const result = verifyPKCESupport(metadata);
|
|
125
|
+
// Assert
|
|
126
|
+
expect(result).toEqual({ supported: true, advertised: true });
|
|
127
|
+
});
|
|
128
|
+
it("should return supported=false when S256 is not in supported methods", () => {
|
|
129
|
+
// Arrange
|
|
130
|
+
const metadata = {
|
|
131
|
+
issuer: "http://localhost:3001",
|
|
132
|
+
authorization_endpoint: "http://localhost:3001/authorize",
|
|
133
|
+
token_endpoint: "http://localhost:3001/token",
|
|
134
|
+
code_challenge_methods_supported: ["plain"],
|
|
135
|
+
};
|
|
136
|
+
// Act
|
|
137
|
+
const result = verifyPKCESupport(metadata);
|
|
138
|
+
// Assert
|
|
139
|
+
expect(result).toEqual({ supported: false, advertised: true });
|
|
140
|
+
});
|
|
141
|
+
it("should return supported=true, advertised=false when code_challenge_methods_supported is missing", () => {
|
|
142
|
+
// Arrange
|
|
143
|
+
const metadata = {
|
|
144
|
+
issuer: "http://localhost:3001",
|
|
145
|
+
authorization_endpoint: "http://localhost:3001/authorize",
|
|
146
|
+
token_endpoint: "http://localhost:3001/token",
|
|
147
|
+
};
|
|
148
|
+
// Act
|
|
149
|
+
const result = verifyPKCESupport(metadata);
|
|
150
|
+
// Assert
|
|
151
|
+
expect(result).toEqual({ supported: true, advertised: false });
|
|
152
|
+
});
|
|
153
|
+
it("should return supported=true, advertised=false when code_challenge_methods_supported is empty", () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const metadata = {
|
|
156
|
+
issuer: "http://localhost:3001",
|
|
157
|
+
authorization_endpoint: "http://localhost:3001/authorize",
|
|
158
|
+
token_endpoint: "http://localhost:3001/token",
|
|
159
|
+
code_challenge_methods_supported: [],
|
|
160
|
+
};
|
|
161
|
+
// Act
|
|
162
|
+
const result = verifyPKCESupport(metadata);
|
|
163
|
+
// Assert
|
|
164
|
+
expect(result).toEqual({ supported: true, advertised: false });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("buildAuthorizationUrl", () => {
|
|
168
|
+
it("should build URL with all required parameters", () => {
|
|
169
|
+
// Arrange
|
|
170
|
+
const params = {
|
|
171
|
+
authorizationEndpoint: "http://localhost:3001/authorize",
|
|
172
|
+
clientId: "test-client",
|
|
173
|
+
redirectUri: "http://localhost:3003/callback",
|
|
174
|
+
state: "random-state",
|
|
175
|
+
codeChallenge: "test-challenge",
|
|
176
|
+
codeChallengeMethod: "S256",
|
|
177
|
+
};
|
|
178
|
+
// Act
|
|
179
|
+
const result = buildAuthorizationUrl(params);
|
|
180
|
+
// Assert
|
|
181
|
+
const url = new URL(result);
|
|
182
|
+
expect(url.origin).toBe("http://localhost:3001");
|
|
183
|
+
expect(url.pathname).toBe("/authorize");
|
|
184
|
+
expect(url.searchParams.get("response_type")).toBe("code");
|
|
185
|
+
expect(url.searchParams.get("client_id")).toBe("test-client");
|
|
186
|
+
expect(url.searchParams.get("redirect_uri")).toBe("http://localhost:3003/callback");
|
|
187
|
+
expect(url.searchParams.get("state")).toBe("random-state");
|
|
188
|
+
expect(url.searchParams.get("code_challenge")).toBe("test-challenge");
|
|
189
|
+
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
|
|
190
|
+
});
|
|
191
|
+
it("should include scope when provided", () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const params = {
|
|
194
|
+
authorizationEndpoint: "http://localhost:3001/authorize",
|
|
195
|
+
clientId: "test-client",
|
|
196
|
+
redirectUri: "http://localhost:3003/callback",
|
|
197
|
+
state: "random-state",
|
|
198
|
+
codeChallenge: "test-challenge",
|
|
199
|
+
codeChallengeMethod: "S256",
|
|
200
|
+
scope: "read write",
|
|
201
|
+
};
|
|
202
|
+
// Act
|
|
203
|
+
const result = buildAuthorizationUrl(params);
|
|
204
|
+
// Assert
|
|
205
|
+
const url = new URL(result);
|
|
206
|
+
expect(url.searchParams.get("scope")).toBe("read write");
|
|
207
|
+
});
|
|
208
|
+
it("should include resource when provided", () => {
|
|
209
|
+
// Arrange
|
|
210
|
+
const params = {
|
|
211
|
+
authorizationEndpoint: "http://localhost:3001/authorize",
|
|
212
|
+
clientId: "test-client",
|
|
213
|
+
redirectUri: "http://localhost:3003/callback",
|
|
214
|
+
state: "random-state",
|
|
215
|
+
codeChallenge: "test-challenge",
|
|
216
|
+
codeChallengeMethod: "S256",
|
|
217
|
+
resource: "http://localhost:3001/mcp",
|
|
218
|
+
};
|
|
219
|
+
// Act
|
|
220
|
+
const result = buildAuthorizationUrl(params);
|
|
221
|
+
// Assert
|
|
222
|
+
const url = new URL(result);
|
|
223
|
+
expect(url.searchParams.get("resource")).toBe("http://localhost:3001/mcp");
|
|
224
|
+
});
|
|
225
|
+
it("should not include scope when not provided", () => {
|
|
226
|
+
// Arrange
|
|
227
|
+
const params = {
|
|
228
|
+
authorizationEndpoint: "http://localhost:3001/authorize",
|
|
229
|
+
clientId: "test-client",
|
|
230
|
+
redirectUri: "http://localhost:3003/callback",
|
|
231
|
+
state: "random-state",
|
|
232
|
+
codeChallenge: "test-challenge",
|
|
233
|
+
codeChallengeMethod: "S256",
|
|
234
|
+
};
|
|
235
|
+
// Act
|
|
236
|
+
const result = buildAuthorizationUrl(params);
|
|
237
|
+
// Assert
|
|
238
|
+
const url = new URL(result);
|
|
239
|
+
expect(url.searchParams.has("scope")).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe("getCanonicalResourceUri", () => {
|
|
243
|
+
it("should return URL without trailing slash", () => {
|
|
244
|
+
// Arrange
|
|
245
|
+
const mcpServerUrl = "http://localhost:3001/mcp/";
|
|
246
|
+
// Act
|
|
247
|
+
const result = getCanonicalResourceUri(mcpServerUrl);
|
|
248
|
+
// Assert
|
|
249
|
+
expect(result).toBe("http://localhost:3001/mcp");
|
|
250
|
+
});
|
|
251
|
+
it("should keep URL without trailing slash unchanged", () => {
|
|
252
|
+
// Arrange
|
|
253
|
+
const mcpServerUrl = "http://localhost:3001/mcp";
|
|
254
|
+
// Act
|
|
255
|
+
const result = getCanonicalResourceUri(mcpServerUrl);
|
|
256
|
+
// Assert
|
|
257
|
+
expect(result).toBe("http://localhost:3001/mcp");
|
|
258
|
+
});
|
|
259
|
+
it("should handle root URL correctly", () => {
|
|
260
|
+
// Arrange
|
|
261
|
+
const mcpServerUrl = "http://localhost:3001/";
|
|
262
|
+
// Act
|
|
263
|
+
const result = getCanonicalResourceUri(mcpServerUrl);
|
|
264
|
+
// Assert
|
|
265
|
+
expect(result).toBe("http://localhost:3001/");
|
|
266
|
+
});
|
|
267
|
+
it("should strip query parameters", () => {
|
|
268
|
+
// Arrange
|
|
269
|
+
const mcpServerUrl = "http://localhost:3001/mcp?param=value";
|
|
270
|
+
// Act
|
|
271
|
+
const result = getCanonicalResourceUri(mcpServerUrl);
|
|
272
|
+
// Assert
|
|
273
|
+
expect(result).toBe("http://localhost:3001/mcp");
|
|
274
|
+
});
|
|
275
|
+
it("should strip fragment", () => {
|
|
276
|
+
// Arrange
|
|
277
|
+
const mcpServerUrl = "http://localhost:3001/mcp#section";
|
|
278
|
+
// Act
|
|
279
|
+
const result = getCanonicalResourceUri(mcpServerUrl);
|
|
280
|
+
// Assert
|
|
281
|
+
expect(result).toBe("http://localhost:3001/mcp");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|