Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions packages/event-handler/src/appsync-events/Router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
import { isRecord } from '@aws-lambda-powertools/commons/typeutils';
import {
getStringFromEnv,
isDevMode,
} from '@aws-lambda-powertools/commons/utils/env';
import { getStringFromEnv, isDevMode } from '@aws-lambda-powertools/commons/utils/env';
import type {
OnPublishHandler,
OnSubscribeHandler,
Expand Down Expand Up @@ -34,7 +31,7 @@ class Router {
* Whether the router is running in development mode.
*/
protected readonly isDev: boolean = false;

public constructor(options?: RouterOptions) {
const alcLogLevel = getStringFromEnv({
key: 'AWS_LAMBDA_LOG_LEVEL',
Expand Down
2 changes: 1 addition & 1 deletion packages/event-handler/src/appsync-graphql/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js';
export {
InvalidBatchResponseException,
ResolverNotFoundException,
InvalidBatchResponseException,
} from './errors.js';
export {
awsDate,
Expand Down
1 change: 0 additions & 1 deletion packages/event-handler/src/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export {
ServiceUnavailableError,
UnauthorizedError,
} from './errors.js';
export { cors } from './middleware/index.js';
export { Router } from './Router.js';
export {
composeMiddleware,
Expand Down
197 changes: 51 additions & 146 deletions packages/event-handler/src/rest/middleware/cors.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,24 @@
import type {
CorsOptions,
HandlerResponse,
Middleware,
RequestContext,
} from '../../types/rest.js';
import {
DEFAULT_CORS_OPTIONS,
HttpErrorCodes,
HttpVerbs,
} from '../constants.js';

/**
* Resolved CORS configuration with all defaults applied
*/
type ResolvedCorsConfig = {
origin: CorsOptions['origin'];
allowMethods: string[];
allowHeaders: string[];
exposeHeaders: string[];
credentials: boolean;
maxAge?: number;
};

/**
* Resolves and validates the CORS configuration
*/
const resolveConfiguration = (userOptions: CorsOptions): ResolvedCorsConfig => {
return {
origin: userOptions.origin ?? DEFAULT_CORS_OPTIONS.origin,
allowMethods: userOptions.allowMethods ?? [
...DEFAULT_CORS_OPTIONS.allowMethods,
],
allowHeaders: userOptions.allowHeaders ?? [
...DEFAULT_CORS_OPTIONS.allowHeaders,
],
exposeHeaders: userOptions.exposeHeaders ?? [
...DEFAULT_CORS_OPTIONS.exposeHeaders,
],
credentials: userOptions.credentials ?? DEFAULT_CORS_OPTIONS.credentials,
maxAge: userOptions.maxAge,
};
};

/**
* Resolves the origin value based on the configuration
*/
const resolveOrigin = (
originConfig: CorsOptions['origin'],
requestOrigin: string | null | undefined,
reqCtx: RequestContext
originConfig: NonNullable<CorsOptions['origin']>,
requestOrigin: string | null,
): string => {
const origin = requestOrigin || undefined;

if (typeof originConfig === 'function') {
const result = originConfig(origin, reqCtx);
if (typeof result === 'boolean') {
return result ? origin || '*' : '';
}
return result;
}

if (Array.isArray(originConfig)) {
return origin && originConfig.includes(origin) ? origin : '';
}

if (typeof originConfig === 'string') {
return originConfig;
}

return DEFAULT_CORS_OPTIONS.origin;
};

/**
* Handles preflight OPTIONS requests
*/
const handlePreflight = (
config: ResolvedCorsConfig,
reqCtx: RequestContext
): Response => {
const { request, res } = reqCtx;
const requestOrigin = request.headers.get('Origin');
const resolvedOrigin = resolveOrigin(config.origin, requestOrigin, reqCtx);

// Mutate existing response headers
if (resolvedOrigin) {
res.headers.set('Access-Control-Allow-Origin', resolvedOrigin);
}

if (config.allowMethods.length > 0) {
res.headers.set(
'Access-Control-Allow-Methods',
config.allowMethods.join(', ')
);
}

if (config.allowHeaders.length > 0) {
res.headers.set(
'Access-Control-Allow-Headers',
config.allowHeaders.join(', ')
);
}

if (config.credentials) {
res.headers.set('Access-Control-Allow-Credentials', 'true');
}

if (config.maxAge !== undefined) {
res.headers.set('Access-Control-Max-Age', config.maxAge.toString());
}

return new Response(null, {
status: HttpErrorCodes.NO_CONTENT,
headers: res.headers,
});
};

/**
* Adds CORS headers to regular requests
*/
const addCorsHeaders = (
config: ResolvedCorsConfig,
reqCtx: RequestContext
): void => {
const { request, res } = reqCtx;
const requestOrigin = request.headers.get('Origin');
const resolvedOrigin = resolveOrigin(config.origin, requestOrigin, reqCtx);

if (resolvedOrigin) {
res.headers.set('Access-Control-Allow-Origin', resolvedOrigin);
}

if (config.exposeHeaders.length > 0) {
res.headers.set(
'Access-Control-Expose-Headers',
config.exposeHeaders.join(', ')
);
}

if (config.credentials) {
res.headers.set('Access-Control-Allow-Credentials', 'true');
return requestOrigin && originConfig.includes(requestOrigin) ? requestOrigin : '';
}
return originConfig;
};

/**
Expand All @@ -148,8 +27,11 @@ const addCorsHeaders = (
*
* @example
* ```typescript
* import { cors } from '@aws-lambda-powertools/event-handler/rest';
*
* import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
* import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
*
* const app = new Router();
*
* // Use default configuration
* app.use(cors());
*
Expand All @@ -168,28 +50,51 @@ const addCorsHeaders = (
* }
* }));
* ```
* @param options - CORS configuration options
*
* @param options.origin - The origin to allow requests from
* @param options.allowMethods - The HTTP methods to allow
* @param options.allowHeaders - The headers to allow
* @param options.exposeHeaders - The headers to expose
* @param options.credentials - Whether to allow credentials
* @param options.maxAge - The maximum age for the preflight response
*/
export const cors = (options: CorsOptions = {}): Middleware => {
const config = resolveConfiguration(options);

return async (
_params: Record<string, string>,
reqCtx: RequestContext,
next: () => Promise<HandlerResponse | void>
) => {
const { request } = reqCtx;
const method = request.method.toUpperCase();

export const cors = (options?: CorsOptions): Middleware => {
const config = {
...DEFAULT_CORS_OPTIONS,
...options
};

return async (_params, reqCtx, next) => {
const requestOrigin = reqCtx.request.headers.get('Origin');
const resolvedOrigin = resolveOrigin(config.origin, requestOrigin);

reqCtx.res.headers.set('access-control-allow-origin', resolvedOrigin);
if (resolvedOrigin !== '*') {
reqCtx.res.headers.set('Vary', 'Origin');
}
config.allowMethods.forEach(method => {
reqCtx.res.headers.append('access-control-allow-methods', method);
});
config.allowHeaders.forEach(header => {
reqCtx.res.headers.append('access-control-allow-headers', header);
});
config.exposeHeaders.forEach(header => {
reqCtx.res.headers.append('access-control-expose-headers', header);
});
reqCtx.res.headers.set('access-control-allow-credentials', config.credentials.toString());
if (config.maxAge !== undefined) {
reqCtx.res.headers.set('access-control-max-age', config.maxAge.toString());
}

// Handle preflight OPTIONS request
if (method === HttpVerbs.OPTIONS) {
return handlePreflight(config, reqCtx);
if (reqCtx.request.method === HttpVerbs.OPTIONS && reqCtx.request.headers.has('Access-Control-Request-Method')) {
return new Response(null, {
status: HttpErrorCodes.NO_CONTENT,
headers: reqCtx.res.headers,
});
}
// Continue to next middleware/handler first

// Continue to next middleware/handler
await next();

// Add CORS headers to the response after handler
addCorsHeaders(config, reqCtx);
};
};
10 changes: 2 additions & 8 deletions packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,10 @@ type ValidationResult = {
type CorsOptions = {
/**
* The Access-Control-Allow-Origin header value.
* Can be a string, array of strings, or a function that returns a string or boolean.
* Can be a string, array of strings.
* @default '*'
*/
origin?:
| string
| string[]
| ((
origin: string | undefined,
reqCtx: RequestContext
) => string | boolean);
origin?: string | string[];

/**
* The Access-Control-Allow-Methods header value.
Expand Down

This file was deleted.

5 changes: 3 additions & 2 deletions packages/event-handler/tests/unit/rest/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { Middleware } from '../../../src/types/rest.js';

export const createTestEvent = (
path: string,
httpMethod: string
httpMethod: string,
headers: Record<string, string> = {}
): APIGatewayProxyEvent => ({
path,
httpMethod,
headers: {},
headers,
body: null,
multiValueHeaders: {},
isBase64Encoded: false,
Expand Down
Loading