diff --git a/.gitignore b/.gitignore index 694735b6..4e004036 100644 --- a/.gitignore +++ b/.gitignore @@ -131,5 +131,9 @@ out .yarn/install-state.gz .pnp.* +# IDE +.vscode/ +.idea/ + .DS_Store dist/ diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 4fce9976..bf9accbe 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -13,8 +13,21 @@ describe("SSEClientTransport", () => { let resourceBaseUrl: URL; let authBaseUrl: URL; let lastServerRequest: IncomingMessage; + const serverRequests: Record = {}; let sendServerMessage: ((message: string) => void) | null = null; + const recordServerRequest = (req: IncomingMessage, res: ServerResponse) => { + lastServerRequest = req; + + const key = `${req.method} ${req.url}`; + serverRequests[key] = serverRequests[key] || []; + serverRequests[key].push(req); + + res.on('finish', () => { + console.log(`[server] ${req.method} ${req.url} -> ${res.statusCode} ${res.statusMessage}`); + }); + }; + beforeEach((done) => { // Reset state lastServerRequest = null as unknown as IncomingMessage; @@ -613,6 +626,8 @@ describe("SSEClientTransport", () => { authServer.close(); authServer = createServer((req, res) => { + recordServerRequest(req, res); + if (req.url && authServerMetadataUrls.includes(req.url)) { res.writeHead(404).end(); return; @@ -625,7 +640,7 @@ describe("SSEClientTransport", () => { req.on("end", () => { const params = new URLSearchParams(body); if (params.get("grant_type") === "refresh_token" && - params.get("refresh_token") === "refresh-token" && + params.get("refresh_token")?.includes("refresh-token") && params.get("client_id") === "test-client-id" && params.get("client_secret") === "test-client-secret") { res.writeHead(200, { "Content-Type": "application/json" }); @@ -656,6 +671,7 @@ describe("SSEClientTransport", () => { let connectionAttempts = 0; resourceServer = createServer((req, res) => { + recordServerRequest(req, res); lastServerRequest = req; if (req.url === "/.well-known/oauth-protected-resource") { @@ -705,6 +721,14 @@ describe("SSEClientTransport", () => { transport = new SSEClientTransport(resourceBaseUrl, { authProvider: mockAuthProvider, + eventSourceInit: { + fetch: (url, init) => { + return fetch(url, { ...init, headers: { + ...(init?.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init?.headers), + 'X-Custom-Header': 'custom-value' + } }); + } + }, }); await transport.start(); @@ -716,6 +740,9 @@ describe("SSEClientTransport", () => { }); expect(connectionAttempts).toBe(1); expect(lastServerRequest.headers.authorization).toBe("Bearer new-token"); + expect(serverRequests["GET /"]).toHaveLength(2); + expect(serverRequests["GET /"] + .every(req => req.headers["x-custom-header"] === "custom-value")).toBe(true); }); it("refreshes expired token during POST request", async () => { @@ -1145,11 +1172,11 @@ describe("SSEClientTransport", () => { return { get redirectUrl() { return "http://localhost/callback"; }, - get clientMetadata() { - return { + get clientMetadata() { + return { redirect_uris: ["http://localhost/callback"], client_name: "Test Client" - }; + }; }, clientInformation: jest.fn().mockResolvedValue(clientInfo), tokens: jest.fn().mockResolvedValue(tokens), @@ -1175,7 +1202,7 @@ describe("SSEClientTransport", () => { })); return; } - + if (req.url === "/token" && req.method === "POST") { // Handle token exchange request let body = ""; @@ -1198,7 +1225,7 @@ describe("SSEClientTransport", () => { }); return; } - + res.writeHead(404).end(); }); @@ -1302,14 +1329,14 @@ describe("SSEClientTransport", () => { // Verify custom fetch was used expect(customFetch).toHaveBeenCalled(); - + // Verify specific OAuth endpoints were called with custom fetch const customFetchCalls = customFetch.mock.calls; const callUrls = customFetchCalls.map(([url]) => url.toString()); - + // Should have called resource metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-protected-resource'))).toBe(true); - + // Should have called OAuth authorization server metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-authorization-server'))).toBe(true); @@ -1375,19 +1402,19 @@ describe("SSEClientTransport", () => { // Verify custom fetch was used expect(customFetch).toHaveBeenCalled(); - + // Verify specific OAuth endpoints were called with custom fetch const customFetchCalls = customFetch.mock.calls; const callUrls = customFetchCalls.map(([url]) => url.toString()); - + // Should have called resource metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-protected-resource'))).toBe(true); - + // Should have called OAuth authorization server metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-authorization-server'))).toBe(true); // Should have attempted the POST request that triggered the 401 - const postCalls = customFetchCalls.filter(([url, options]) => + const postCalls = customFetchCalls.filter(([url, options]) => url.toString() === resourceBaseUrl.href && options?.method === "POST" ); expect(postCalls.length).toBeGreaterThan(0); @@ -1417,19 +1444,19 @@ describe("SSEClientTransport", () => { // Verify custom fetch was used expect(customFetch).toHaveBeenCalled(); - + // Verify specific OAuth endpoints were called with custom fetch const customFetchCalls = customFetch.mock.calls; const callUrls = customFetchCalls.map(([url]) => url.toString()); - + // Should have called resource metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-protected-resource'))).toBe(true); - + // Should have called OAuth authorization server metadata discovery expect(callUrls.some(url => url.includes('/.well-known/oauth-authorization-server'))).toBe(true); // Should have called token endpoint for authorization code exchange - const tokenCalls = customFetchCalls.filter(([url, options]) => + const tokenCalls = customFetchCalls.filter(([url, options]) => url.toString().includes('/token') && options?.method === "POST" ); expect(tokenCalls.length).toBeGreaterThan(0); diff --git a/src/client/sse.ts b/src/client/sse.ts index e1c86ccd..deffeaba 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -35,11 +35,6 @@ export type SSEClientTransportOptions = { /** * Customizes the initial SSE request to the server (the request that begins the stream). - * - * NOTE: Setting this property will prevent an `Authorization` header from - * being automatically attached to the SSE request, if an `authProvider` is - * also given. This can be worked around by setting the `Authorization` header - * manually. */ eventSourceInit?: EventSourceInit;