diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0c888f6..32c1253 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,8 +27,8 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g. iOS] - Browser [e.g. chrome, safari] -- authkit-nextjs version [e.g. 0.12.0] -- Next.js version [e.g. 14.2.5] +- authkit-tanstack-start version [e.g. 0.12.0] +- Tanstack Start version [e.g. 14.2.5] **Additional context** Add any other context about the problem here. diff --git a/.oxlintrc.json b/.oxlintrc.json index 6bb25f6..594d019 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,7 +6,7 @@ "ignorePatterns": ["dist/", "example/src/routeTree.gen.ts"], "overrides": [ { - "files": ["src/server/actions.ts", "src/server/server-functions.ts"], + "files": ["src/server/actions.ts", "src/server/server-functions.ts", "src/server/middleware.ts"], "rules": { "no-restricted-imports": [ "error", @@ -19,10 +19,11 @@ "./context*", "./headers-bag*", "./action-bodies*", - "./server-fn-bodies*" + "./server-fn-bodies*", + "./middleware-body*" ], "allowTypeImports": true, - "message": "Static value imports of server-only modules are forbidden in this file. Move logic into action-bodies.ts or server-fn-bodies.ts and use a dynamic import inside the handler. See CLAUDE.md." + "message": "Static value imports of server-only modules are forbidden in this file. Move logic into action-bodies.ts, server-fn-bodies.ts, or middleware-body.ts and use a dynamic import inside the handler. See CLAUDE.md." }, { "group": ["@workos/authkit-session", "@workos-inc/node"], diff --git a/scripts/check-bundle-leak.sh b/scripts/check-bundle-leak.sh index 0d31dce..16821f4 100755 --- a/scripts/check-bundle-leak.sh +++ b/scripts/check-bundle-leak.sh @@ -18,6 +18,7 @@ FINGERPRINTS=( "@workos-inc/node" "iron-session" "iron-webcrypto" + "eventemitter3" # Code fingerprints (more robust against minification) "FeatureFlagsRuntimeClient" "The listener must be a function" diff --git a/src/server/middleware-body.spec.ts b/src/server/middleware-body.spec.ts new file mode 100644 index 0000000..524785b --- /dev/null +++ b/src/server/middleware-body.spec.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockAuthkit = { + withAuth: vi.fn(), + saveSession: vi.fn(), +}; + +const mockGetConfig = vi.fn(); + +vi.mock('./authkit-loader', () => ({ + getAuthkit: vi.fn(() => Promise.resolve(mockAuthkit)), + validateConfig: vi.fn(() => Promise.resolve()), + getConfig: () => mockGetConfig(), +})); + +import { middlewareBody } from './middleware-body'; + +describe('middlewareBody', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('header merging', () => { + it('uses Headers API for pendingHeaders', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }); + + const args = { + request: mockRequest, + next: vi.fn().mockResolvedValue({ response: mockResponse }), + }; + + const result = await middlewareBody(args); + + expect(result.response).toBe(mockResponse); + }); + + it('appends Set-Cookie headers correctly', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + context.__setPendingHeader('Set-Cookie', 'session=abc123; Path=/'); + return { response: mockResponse }; + }), + }; + + const result = await middlewareBody(args); + + expect(result.response.headers.get('Set-Cookie')).toBe('session=abc123; Path=/'); + }); + + it('supports multiple Set-Cookie headers via append', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + context.__setPendingHeader('Set-Cookie', 'cookie1=value1; Path=/'); + context.__setPendingHeader('Set-Cookie', 'cookie2=value2; Path=/'); + return { response: mockResponse }; + }), + }; + + const result = await middlewareBody(args); + + const cookies = result.response.headers.get('Set-Cookie'); + expect(cookies).toContain('cookie1=value1'); + expect(cookies).toContain('cookie2=value2'); + }); + + it('uses set() for non-cookie headers', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + context.__setPendingHeader('X-Custom', 'value1'); + context.__setPendingHeader('X-Custom', 'value2'); + return { response: mockResponse }; + }), + }; + + const result = await middlewareBody(args); + + expect(result.response.headers.get('X-Custom')).toBe('value2'); + }); + + it('handles refreshed session data via storage context', async () => { + const refreshedData = 'encrypted_session_data'; + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: { id: 'user_123' } }, + refreshedSessionData: refreshedData, + }); + mockAuthkit.saveSession.mockResolvedValue({ + response: new Response('', { + headers: { 'Set-Cookie': 'wos-session=new_value; Path=/' }, + }), + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + const args = { + request: mockRequest, + next: vi.fn(async () => ({ response: mockResponse })), + }; + + const result = await middlewareBody(args); + + expect(mockAuthkit.saveSession).toHaveBeenCalledWith(undefined, refreshedData); + expect(result.response.headers.get('Set-Cookie')).toContain('wos-session=new_value'); + }); + + it('provides correct context shape to downstream handlers', async () => { + const mockAuth = { user: { id: 'user_123' }, sessionId: 'session_123' }; + mockAuthkit.withAuth.mockResolvedValue({ + auth: mockAuth, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + let capturedContext: any = null; + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + capturedContext = context; + return { response: mockResponse }; + }), + }; + + await middlewareBody(args); + + expect(capturedContext.request).toBe(mockRequest); + expect(typeof capturedContext.auth).toBe('function'); + expect(capturedContext.auth()).toBe(mockAuth); + expect(typeof capturedContext.__setPendingHeader).toBe('function'); + }); + + it('preserves existing response headers', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { + status: 200, + headers: { 'X-Existing': 'preserved' }, + }); + + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + context.__setPendingHeader('X-New', 'added'); + return { response: mockResponse }; + }), + }; + + const result = await middlewareBody(args); + + expect(result.response.headers.get('X-Existing')).toBe('preserved'); + expect(result.response.headers.get('X-New')).toBe('added'); + }); + }); + + describe('redirectUri option', () => { + it('passes redirectUri to context when provided', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + let capturedContext: any = null; + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + capturedContext = context; + return { response: mockResponse }; + }), + }; + + await middlewareBody(args, { redirectUri: 'https://custom.example.com/callback' }); + + expect(capturedContext.redirectUri).toBe('https://custom.example.com/callback'); + }); + + it('passes undefined redirectUri when not provided', async () => { + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + mockGetConfig.mockResolvedValue(undefined); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + let capturedContext: any = null; + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + capturedContext = context; + return { response: mockResponse }; + }), + }; + + await middlewareBody(args); + + expect(capturedContext.redirectUri).toBeUndefined(); + }); + + it('uses WORKOS_REDIRECT_URI from config when option not provided', async () => { + const envRedirectUri = 'https://env.example.com/callback'; + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + mockGetConfig.mockResolvedValue(envRedirectUri); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + let capturedContext: any = null; + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + capturedContext = context; + return { response: mockResponse }; + }), + }; + + await middlewareBody(args); + + expect(capturedContext.redirectUri).toBe(envRedirectUri); + }); + + it('prioritizes explicit option over config', async () => { + const explicitRedirectUri = 'https://explicit.example.com/callback'; + mockAuthkit.withAuth.mockResolvedValue({ + auth: { user: null }, + refreshedSessionData: null, + }); + mockGetConfig.mockResolvedValue('https://env.example.com/callback'); + + const mockRequest = new Request('http://test.local'); + const mockResponse = new Response('OK', { status: 200 }); + + let capturedContext: any = null; + const args = { + request: mockRequest, + next: vi.fn(async ({ context }: any) => { + capturedContext = context; + return { response: mockResponse }; + }), + }; + + await middlewareBody(args, { redirectUri: explicitRedirectUri }); + + expect(capturedContext.redirectUri).toBe(explicitRedirectUri); + }); + }); +}); diff --git a/src/server/middleware-body.ts b/src/server/middleware-body.ts new file mode 100644 index 0000000..9aa9776 --- /dev/null +++ b/src/server/middleware-body.ts @@ -0,0 +1,60 @@ +import { getAuthkit, validateConfig, getConfig } from './authkit-loader.js'; +import type { AuthKitMiddlewareOptions } from './types.js'; + +let configValidated = false; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function middlewareBody(args: any, options?: AuthKitMiddlewareOptions) { + const authkit = await getAuthkit(); + + if (!configValidated) { + await validateConfig(); + configValidated = true; + } + + const { auth, refreshedSessionData } = await authkit.withAuth(args.request); + const pendingHeaders = new Headers(); + + const result = await args.next({ + context: { + auth: () => auth, + request: args.request, + redirectUri: options?.redirectUri ?? (await getConfig('redirectUri')), + __setPendingHeader: (key: string, value: string) => { + if (key.toLowerCase() === 'set-cookie') { + pendingHeaders.append(key, value); + } else { + pendingHeaders.set(key, value); + } + }, + }, + }); + + if (refreshedSessionData) { + const { response: sessionResponse } = await authkit.saveSession(undefined, refreshedSessionData); + for (const cookie of sessionResponse?.headers.getSetCookie() ?? []) { + pendingHeaders.append('Set-Cookie', cookie); + } + } + + const headerEntries = [...pendingHeaders]; + if (headerEntries.length === 0) { + return result; + } + + const newResponse = new Response(result.response.body, { + status: result.response.status, + statusText: result.response.statusText, + headers: result.response.headers, + }); + + for (const [key, value] of headerEntries) { + if (key.toLowerCase() === 'set-cookie') { + newResponse.headers.append(key, value); + } else { + newResponse.headers.set(key, value); + } + } + + return { ...result, response: newResponse }; +} diff --git a/src/server/middleware.spec.ts b/src/server/middleware.spec.ts index 11182a5..d306d4a 100644 --- a/src/server/middleware.spec.ts +++ b/src/server/middleware.spec.ts @@ -1,17 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -// Mock dependencies before imports -const mockAuthkit = { - withAuth: vi.fn(), - saveSession: vi.fn(), -}; +const mockMiddlewareBody = vi.fn(); -const mockGetConfig = vi.fn(); - -vi.mock('./authkit-loader', () => ({ - getAuthkit: vi.fn(() => Promise.resolve(mockAuthkit)), - validateConfig: vi.fn(() => Promise.resolve()), - getConfig: () => mockGetConfig(), +vi.mock('./middleware-body', () => ({ + middlewareBody: (...args: unknown[]) => mockMiddlewareBody(...args), })); let middlewareServerCallback: any = null; @@ -33,299 +25,26 @@ describe('authkitMiddleware', () => { middlewareServerCallback = null; }); - describe('header merging', () => { - it('uses Headers API for pendingHeaders', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - }); - - const args = { - request: mockRequest, - next: vi.fn().mockResolvedValue({ response: mockResponse }), - }; - - const result = await middlewareServerCallback(args); - - // No headers to add, should return original result - expect(result.response).toBe(mockResponse); - }); - - it('appends Set-Cookie headers correctly', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - // Simulate action setting cookie via context - context.__setPendingHeader('Set-Cookie', 'session=abc123; Path=/'); - return { response: mockResponse }; - }), - }; - - const result = await middlewareServerCallback(args); - - expect(result.response.headers.get('Set-Cookie')).toBe('session=abc123; Path=/'); - }); - - it('supports multiple Set-Cookie headers via append', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - // Simulate multiple cookies being set - context.__setPendingHeader('Set-Cookie', 'cookie1=value1; Path=/'); - context.__setPendingHeader('Set-Cookie', 'cookie2=value2; Path=/'); - return { response: mockResponse }; - }), - }; - - const result = await middlewareServerCallback(args); - - // Headers API getAll or iterate to check multiple values - const cookies = result.response.headers.get('Set-Cookie'); - // Note: Headers.get() concatenates multiple values with ", " - expect(cookies).toContain('cookie1=value1'); - expect(cookies).toContain('cookie2=value2'); - }); - - it('uses set() for non-cookie headers', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - context.__setPendingHeader('X-Custom', 'value1'); - context.__setPendingHeader('X-Custom', 'value2'); // Should overwrite - return { response: mockResponse }; - }), - }; - - const result = await middlewareServerCallback(args); - - expect(result.response.headers.get('X-Custom')).toBe('value2'); - }); - - it('handles refreshed session data via storage context', async () => { - const refreshedData = 'encrypted_session_data'; - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: { id: 'user_123' } }, - refreshedSessionData: refreshedData, - }); - mockAuthkit.saveSession.mockResolvedValue({ - headers: { 'Set-Cookie': 'wos-session=new_value; Path=/' }, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - const args = { - request: mockRequest, - next: vi.fn(async () => ({ response: mockResponse })), - }; - - await middlewareServerCallback(args); - - expect(mockAuthkit.saveSession).toHaveBeenCalledWith(undefined, refreshedData); - }); + it('delegates to middlewareBody via dynamic import', async () => { + mockMiddlewareBody.mockResolvedValue({ response: new Response() }); - it('provides correct context shape to downstream handlers', async () => { - const mockAuth = { user: { id: 'user_123' }, sessionId: 'session_123' }; - mockAuthkit.withAuth.mockResolvedValue({ - auth: mockAuth, - refreshedSessionData: null, - }); + authkitMiddleware(); - authkitMiddleware(); + const mockArgs = { request: new Request('http://test.local'), next: vi.fn() }; + await middlewareServerCallback(mockArgs); - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - let capturedContext: any = null; - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - capturedContext = context; - return { response: mockResponse }; - }), - }; - - await middlewareServerCallback(args); - - expect(capturedContext.request).toBe(mockRequest); - expect(typeof capturedContext.auth).toBe('function'); - expect(capturedContext.auth()).toBe(mockAuth); - expect(typeof capturedContext.__setPendingHeader).toBe('function'); - }); - - it('preserves existing response headers', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { - status: 200, - headers: { 'X-Existing': 'preserved' }, - }); - - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - context.__setPendingHeader('X-New', 'added'); - return { response: mockResponse }; - }), - }; - - const result = await middlewareServerCallback(args); - - expect(result.response.headers.get('X-Existing')).toBe('preserved'); - expect(result.response.headers.get('X-New')).toBe('added'); - }); + expect(mockMiddlewareBody).toHaveBeenCalledWith(mockArgs, undefined); }); - describe('redirectUri option', () => { - it('passes redirectUri to context when provided', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - - authkitMiddleware({ redirectUri: 'https://custom.example.com/callback' }); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - let capturedContext: any = null; - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - capturedContext = context; - return { response: mockResponse }; - }), - }; - - await middlewareServerCallback(args); - - expect(capturedContext.redirectUri).toBe('https://custom.example.com/callback'); - }); - - it('passes undefined redirectUri when not provided', async () => { - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - mockGetConfig.mockResolvedValue(undefined); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - let capturedContext: any = null; - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - capturedContext = context; - return { response: mockResponse }; - }), - }; - - await middlewareServerCallback(args); - - expect(capturedContext.redirectUri).toBeUndefined(); - }); - - it('uses WORKOS_REDIRECT_URI from config when option not provided', async () => { - const envRedirectUri = 'https://env.example.com/callback'; - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - mockGetConfig.mockResolvedValue(envRedirectUri); - - authkitMiddleware(); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); - - let capturedContext: any = null; - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - capturedContext = context; - return { response: mockResponse }; - }), - }; - - await middlewareServerCallback(args); - - expect(capturedContext.redirectUri).toBe(envRedirectUri); - }); - - it('prioritizes explicit option over config', async () => { - const explicitRedirectUri = 'https://explicit.example.com/callback'; - mockAuthkit.withAuth.mockResolvedValue({ - auth: { user: null }, - refreshedSessionData: null, - }); - mockGetConfig.mockResolvedValue('https://env.example.com/callback'); - - authkitMiddleware({ redirectUri: explicitRedirectUri }); - - const mockRequest = new Request('http://test.local'); - const mockResponse = new Response('OK', { status: 200 }); + it('passes options through to middlewareBody', async () => { + mockMiddlewareBody.mockResolvedValue({ response: new Response() }); - let capturedContext: any = null; - const args = { - request: mockRequest, - next: vi.fn(async ({ context }: any) => { - capturedContext = context; - return { response: mockResponse }; - }), - }; + const options = { redirectUri: 'https://custom.example.com/callback' }; + authkitMiddleware(options); - await middlewareServerCallback(args); + const mockArgs = { request: new Request('http://test.local'), next: vi.fn() }; + await middlewareServerCallback(mockArgs); - expect(capturedContext.redirectUri).toBe(explicitRedirectUri); - }); + expect(mockMiddlewareBody).toHaveBeenCalledWith(mockArgs, options); }); }); diff --git a/src/server/middleware.ts b/src/server/middleware.ts index a7a1090..d934186 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,18 +1,7 @@ import { createMiddleware } from '@tanstack/react-start'; -import { getAuthkit, validateConfig, getConfig } from './authkit-loader.js'; +import type { AuthKitMiddlewareOptions } from './types.js'; -let configValidated = false; - -/** - * Options for AuthKit middleware. - */ -export interface AuthKitMiddlewareOptions { - /** - * Override the default redirect URI for OAuth callbacks. - * Useful for dynamic environments like Vercel preview deployments. - */ - redirectUri?: string; -} +export type { AuthKitMiddlewareOptions }; /** * AuthKit middleware for TanStack Start. @@ -38,63 +27,7 @@ export interface AuthKitMiddlewareOptions { */ export const authkitMiddleware = (options?: AuthKitMiddlewareOptions) => { return createMiddleware().server(async (args) => { - const authkit = await getAuthkit(); - - if (!configValidated) { - await validateConfig(); - configValidated = true; - } - - const { auth, refreshedSessionData } = await authkit.withAuth(args.request); - const pendingHeaders = new Headers(); - - const result = await args.next({ - context: { - auth: () => auth, - request: args.request, - redirectUri: options?.redirectUri ?? (await getConfig('redirectUri')), - __setPendingHeader: (key: string, value: string) => { - // Use append for Set-Cookie to support multiple cookies - if (key.toLowerCase() === 'set-cookie') { - pendingHeaders.append(key, value); - } else { - pendingHeaders.set(key, value); - } - }, - }, - }); - - // Apply refreshed session cookie. Context is unavailable after args.next(), - // so saveSession returns headers on the response instead of via context. - if (refreshedSessionData) { - const { response: sessionResponse } = await authkit.saveSession(undefined, refreshedSessionData); - // Extract Set-Cookie headers from response and add to pendingHeaders - const setCookieHeader = sessionResponse?.headers.get('Set-Cookie'); - if (setCookieHeader) { - pendingHeaders.append('Set-Cookie', setCookieHeader); - } - } - - const headerEntries = [...pendingHeaders]; - if (headerEntries.length === 0) { - return result; - } - - const newResponse = new Response(result.response.body, { - status: result.response.status, - statusText: result.response.statusText, - headers: result.response.headers, - }); - - for (const [key, value] of headerEntries) { - // Use append for Set-Cookie to preserve multiple cookie values - if (key.toLowerCase() === 'set-cookie') { - newResponse.headers.append(key, value); - } else { - newResponse.headers.set(key, value); - } - } - - return { ...result, response: newResponse }; + const { middlewareBody } = await import('./middleware-body.js'); + return middlewareBody(args, options); }); }; diff --git a/src/server/types.ts b/src/server/types.ts index 3393c6f..0b28d59 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -1,5 +1,16 @@ import type { User, Impersonator } from '../types.js'; +/** + * Options for AuthKit middleware. + */ +export interface AuthKitMiddlewareOptions { + /** + * Override the default redirect URI for OAuth callbacks. + * Useful for dynamic environments like Vercel preview deployments. + */ + redirectUri?: string; +} + /** * OAuth tokens from upstream identity provider (e.g., Google, Microsoft) * Structure varies by provider but typically includes access_token and optional refresh_token