diff --git a/packages/server/__tests__/openapi-loader.test.ts b/packages/server/__tests__/openapi-loader.test.ts index b92695f..48c88df 100644 --- a/packages/server/__tests__/openapi-loader.test.ts +++ b/packages/server/__tests__/openapi-loader.test.ts @@ -1394,7 +1394,7 @@ paths: }); }); - describe('headerProvider and contextProvider', () => { + describe('headerProvider', () => { it('should accept headerProvider option', async () => { const spec = { openapi: '3.0.0', @@ -1416,10 +1416,48 @@ paths: const apiGroup = await loadOpenAPI(path, { headerProvider }); expect(apiGroup.functions).toHaveLength(1); - // headerProvider is used at runtime, not at load time }); - it('should accept contextProvider option', async () => { + it('should pass requestContext directly to headerProvider', async () => { + const spec = { + openapi: '3.0.0', + info: { title: 'Test', version: '1.0.0' }, + servers: [{ url: 'https://api.example.com' }], + paths: { + '/test': { + get: { + operationId: 'test', + responses: { '200': { description: 'OK' } }, + }, + }, + }, + }; + + const path = await writeSpec('header-provider-context.json', spec); + const headerProvider = vi.fn().mockResolvedValue({ Authorization: 'Bearer tok_123' }); + + const apiGroup = await loadOpenAPI(path, { + headerProvider, + baseURL: 'https://api.example.com', + }); + + const handler = apiGroup.functions![0].handler!; + const requestContext = { userId: 'user-1', headers: { 'x-tenant': 'acme' } }; + + try { + await handler({}, { requestContext, emit: () => {} }); + } catch { + // fetch will fail, we only care about what headerProvider received + } + + expect(headerProvider).toHaveBeenCalledTimes(1); + expect(headerProvider).toHaveBeenCalledWith( + {}, + requestContext + ); + }); + + it('should still accept deprecated contextProvider for backward compatibility', async () => { const spec = { openapi: '3.0.0', info: { title: 'Test', version: '1.0.0' }, diff --git a/packages/server/src/graphql-loader.ts b/packages/server/src/graphql-loader.ts index 0ad2028..0190217 100644 --- a/packages/server/src/graphql-loader.ts +++ b/packages/server/src/graphql-loader.ts @@ -31,11 +31,11 @@ import fs from 'node:fs/promises'; * Dynamic header provider for GraphQL requests. * * @param params - The GraphQL query parameters - * @param context - Optional context from contextProvider (e.g., auth tokens) + * @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens) */ export type GraphQLAuthProvider = ( params?: Record, - context?: Record + requestContext?: Record ) => Promise> | Record; /** @@ -43,12 +43,12 @@ export type GraphQLAuthProvider = ( * Priority: headerProvider > authProvider + auth > static headers * @param options - Load options including auth config * @param params - Optional request params passed to headerProvider for dynamic resolution - * @param executionContext - Optional execution context passed from handler (contains requestContext) + * @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens) */ async function resolveHeaders( options: LoadGraphQLOptions, params?: Record, - executionContext?: Record + requestContext?: Record ): Promise> { const headers: Record = {}; @@ -65,10 +65,11 @@ async function resolveHeaders( } if (options.headerProvider) { - const context = options.contextProvider - ? await options.contextProvider(executionContext) - : undefined; - const dynamicHeaders = await options.headerProvider(params, context); + let ctx = requestContext; + if (options.contextProvider) { + ctx = await options.contextProvider(requestContext); + } + const dynamicHeaders = await options.headerProvider(params, ctx); Object.assign(headers, dynamicHeaders); } @@ -193,10 +194,11 @@ export interface LoadGraphQLOptions { headers?: Record; /** Auth provider for dynamic credential resolution */ authProvider?: AuthProvider; - /** Dynamic header provider function - called before each request */ + /** Dynamic header provider function - called before each request with params and requestContext */ headerProvider?: GraphQLAuthProvider; - /** Context provider - called before each request to get current context (e.g., auth tokens). - * Receives execution context which may contain requestContext from execute() call. */ + /** + * @deprecated Use headerProvider instead, which now receives requestContext directly as its second parameter. + */ contextProvider?: ( executionContext?: Record ) => Record | Promise>; @@ -342,7 +344,7 @@ function convertFieldToFunction( description, inputSchema, outputSchema, - handler: async (params: unknown, context?: { metadata?: Record }) => { + handler: async (params: unknown, context?: { metadata?: Record; requestContext?: Record }) => { const paramsObj = (params as Record) || {}; const customFields = paramsObj._fields; @@ -373,8 +375,7 @@ function convertFieldToFunction( context.metadata.graphql_variables = variables; } - // Pass execution context (including requestContext) to resolveHeaders - const headers = await resolveHeaders(options, paramsObj, context); + const headers = await resolveHeaders(options, paramsObj, context?.requestContext); const fetchFn = options.fetcher || fetch; const response = await fetchFn(url, { diff --git a/packages/server/src/openapi-loader.ts b/packages/server/src/openapi-loader.ts index 24c5383..261e183 100644 --- a/packages/server/src/openapi-loader.ts +++ b/packages/server/src/openapi-loader.ts @@ -166,21 +166,18 @@ export interface LoadOpenAPIOptions { /** * Dynamic header provider for per-request authentication (e.g., per-user OAuth). - * Similar to GraphQL's headerProvider. Called before each API request. + * Called before each API request with the request parameters and the execution's requestContext. * @param params - The request parameters - * @param context - Optional context from contextProvider + * @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens) * @returns Headers to add to the request */ headerProvider?: ( params: Record | undefined, - context?: Record + requestContext?: Record ) => Promise> | Record; /** - * Context provider to extract context from execution environment. - * Similar to GraphQL's contextProvider. Called once per request. - * @param executionContext - The execution context from ATP - * @returns Context object passed to headerProvider + * @deprecated Use headerProvider instead, which now receives requestContext directly as its second parameter. */ contextProvider?: ( executionContext?: Record @@ -515,13 +512,12 @@ function convertOperation( 'Content-Type': 'application/json', }; - let context: Record | undefined; - if (options.contextProvider) { - context = await options.contextProvider(handlerContext?.requestContext); - } - if (options.headerProvider) { - const dynamicHeaders = await options.headerProvider(input, context); + let requestContext = handlerContext?.requestContext; + if (options.contextProvider) { + requestContext = await options.contextProvider(requestContext); + } + const dynamicHeaders = await options.headerProvider(input, requestContext); Object.assign(headers, dynamicHeaders); log.debug('Added headers from headerProvider', { keys: Object.keys(dynamicHeaders) }); }