1
+ import type { Middleware , RequestContext , HandlerResponse } from '../../types/rest.js' ;
2
+ import { HttpErrorCodes , HttpVerbs } from '../constants.js' ;
3
+
4
+ /**
5
+ * Configuration options for CORS middleware
6
+ */
7
+ export interface CorsOptions {
8
+ /**
9
+ * The Access-Control-Allow-Origin header value.
10
+ * Can be a string, array of strings, or a function that returns a string or boolean.
11
+ * @default '*'
12
+ */
13
+ origin ?: string | string [ ] | ( ( origin : string | undefined , reqCtx : RequestContext ) => string | boolean ) ;
14
+
15
+ /**
16
+ * The Access-Control-Allow-Methods header value.
17
+ * @default ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']
18
+ */
19
+ allowMethods ?: string [ ] ;
20
+
21
+ /**
22
+ * The Access-Control-Allow-Headers header value.
23
+ * @default ['Authorization', 'Content-Type', 'X-Amz-Date', 'X-Api-Key', 'X-Amz-Security-Token']
24
+ */
25
+ allowHeaders ?: string [ ] ;
26
+
27
+ /**
28
+ * The Access-Control-Expose-Headers header value.
29
+ * @default []
30
+ */
31
+ exposeHeaders ?: string [ ] ;
32
+
33
+ /**
34
+ * The Access-Control-Allow-Credentials header value.
35
+ * @default false
36
+ */
37
+ credentials ?: boolean ;
38
+
39
+ /**
40
+ * The Access-Control-Max-Age header value in seconds.
41
+ * Only applicable for preflight requests.
42
+ */
43
+ maxAge ?: number ;
44
+ }
45
+
46
+ /**
47
+ * Resolved CORS configuration with all defaults applied
48
+ */
49
+ interface ResolvedCorsConfig {
50
+ origin : CorsOptions [ 'origin' ] ;
51
+ allowMethods : string [ ] ;
52
+ allowHeaders : string [ ] ;
53
+ exposeHeaders : string [ ] ;
54
+ credentials : boolean ;
55
+ maxAge ?: number ;
56
+ }
57
+
58
+ /**
59
+ * Default CORS configuration matching Python implementation
60
+ */
61
+ const DEFAULT_CORS_OPTIONS : Required < Omit < CorsOptions , 'maxAge' > > = {
62
+ origin : '*' ,
63
+ allowMethods : [ 'DELETE' , 'GET' , 'HEAD' , 'PATCH' , 'POST' , 'PUT' ] ,
64
+ allowHeaders : [ 'Authorization' , 'Content-Type' , 'X-Amz-Date' , 'X-Api-Key' , 'X-Amz-Security-Token' ] ,
65
+ exposeHeaders : [ ] ,
66
+ credentials : false ,
67
+ } ;
68
+
69
+ /**
70
+ * Resolves and validates the CORS configuration
71
+ */
72
+ function resolveConfiguration ( userOptions : CorsOptions ) : ResolvedCorsConfig {
73
+ const config : ResolvedCorsConfig = {
74
+ origin : userOptions . origin ?? DEFAULT_CORS_OPTIONS . origin ,
75
+ allowMethods : userOptions . allowMethods ?? DEFAULT_CORS_OPTIONS . allowMethods ,
76
+ allowHeaders : userOptions . allowHeaders ?? DEFAULT_CORS_OPTIONS . allowHeaders ,
77
+ exposeHeaders : userOptions . exposeHeaders ?? DEFAULT_CORS_OPTIONS . exposeHeaders ,
78
+ credentials : userOptions . credentials ?? DEFAULT_CORS_OPTIONS . credentials ,
79
+ maxAge : userOptions . maxAge ,
80
+ } ;
81
+
82
+ return config ;
83
+ }
84
+
85
+ /**
86
+ * Resolves the origin value based on the configuration
87
+ */
88
+ function resolveOrigin (
89
+ originConfig : CorsOptions [ 'origin' ] ,
90
+ requestOrigin : string | null | undefined ,
91
+ reqCtx : RequestContext
92
+ ) : string {
93
+ const origin = requestOrigin || undefined ;
94
+
95
+ if ( typeof originConfig === 'function' ) {
96
+ const result = originConfig ( origin , reqCtx ) ;
97
+ if ( typeof result === 'boolean' ) {
98
+ return result ? ( origin || '*' ) : '' ;
99
+ }
100
+ return result ;
101
+ }
102
+
103
+ if ( Array . isArray ( originConfig ) ) {
104
+ return origin && originConfig . includes ( origin ) ? origin : '' ;
105
+ }
106
+
107
+ if ( typeof originConfig === 'string' ) {
108
+ return originConfig ;
109
+ }
110
+
111
+ return DEFAULT_CORS_OPTIONS . origin as string ;
112
+ }
113
+
114
+ /**
115
+ * Handles preflight OPTIONS requests
116
+ */
117
+ function handlePreflight ( config : ResolvedCorsConfig , reqCtx : RequestContext ) : Response {
118
+ const { request } = reqCtx ;
119
+ const requestOrigin = request . headers . get ( 'Origin' ) ;
120
+ const resolvedOrigin = resolveOrigin ( config . origin , requestOrigin , reqCtx ) ;
121
+
122
+ const headers = new Headers ( ) ;
123
+
124
+ if ( resolvedOrigin ) {
125
+ headers . set ( 'Access-Control-Allow-Origin' , resolvedOrigin ) ;
126
+ }
127
+
128
+ if ( config . allowMethods . length > 0 ) {
129
+ headers . set ( 'Access-Control-Allow-Methods' , config . allowMethods . join ( ', ' ) ) ;
130
+ }
131
+
132
+ if ( config . allowHeaders . length > 0 ) {
133
+ headers . set ( 'Access-Control-Allow-Headers' , config . allowHeaders . join ( ', ' ) ) ;
134
+ }
135
+
136
+ if ( config . credentials ) {
137
+ headers . set ( 'Access-Control-Allow-Credentials' , 'true' ) ;
138
+ }
139
+
140
+ if ( config . maxAge !== undefined ) {
141
+ headers . set ( 'Access-Control-Max-Age' , config . maxAge . toString ( ) ) ;
142
+ }
143
+
144
+ return new Response ( null , {
145
+ status : HttpErrorCodes . NO_CONTENT , // 204
146
+ headers,
147
+ } ) ;
148
+ }
149
+
150
+ /**
151
+ * Adds CORS headers to regular requests
152
+ */
153
+ function addCorsHeaders ( config : ResolvedCorsConfig , reqCtx : RequestContext ) : void {
154
+ const { request, res } = reqCtx ;
155
+ const requestOrigin = request . headers . get ( 'Origin' ) ;
156
+ const resolvedOrigin = resolveOrigin ( config . origin , requestOrigin , reqCtx ) ;
157
+
158
+ if ( resolvedOrigin ) {
159
+ res . headers . set ( 'Access-Control-Allow-Origin' , resolvedOrigin ) ;
160
+ }
161
+
162
+ if ( config . exposeHeaders . length > 0 ) {
163
+ res . headers . set ( 'Access-Control-Expose-Headers' , config . exposeHeaders . join ( ', ' ) ) ;
164
+ }
165
+
166
+ if ( config . credentials ) {
167
+ res . headers . set ( 'Access-Control-Allow-Credentials' , 'true' ) ;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Creates a CORS middleware that adds appropriate CORS headers to responses
173
+ * and handles OPTIONS preflight requests.
174
+ *
175
+ * @param options - CORS configuration options
176
+ * @returns A middleware function that handles CORS
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * import { cors } from '@aws-lambda-powertools/event-handler/rest';
181
+ *
182
+ * // Use default configuration
183
+ * app.use(cors());
184
+ *
185
+ * // Custom configuration
186
+ * app.use(cors({
187
+ * origin: 'https://example.com',
188
+ * allowMethods: ['GET', 'POST'],
189
+ * credentials: true,
190
+ * }));
191
+ *
192
+ * // Dynamic origin with function
193
+ * app.use(cors({
194
+ * origin: (origin, reqCtx) => {
195
+ * const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
196
+ * return origin && allowedOrigins.includes(origin);
197
+ * }
198
+ * }));
199
+ * ```
200
+ */
201
+ export const cors = ( options : CorsOptions = { } ) : Middleware => {
202
+ const config = resolveConfiguration ( options ) ;
203
+
204
+ return async ( _params : Record < string , string > , reqCtx : RequestContext , next : ( ) => Promise < HandlerResponse | void > ) => {
205
+ const { request } = reqCtx ;
206
+ const method = request . method . toUpperCase ( ) ;
207
+
208
+ // Handle preflight OPTIONS request
209
+ if ( method === HttpVerbs . OPTIONS ) {
210
+ return handlePreflight ( config , reqCtx ) ;
211
+ }
212
+
213
+ // Continue to next middleware/handler first
214
+ await next ( ) ;
215
+
216
+ // Add CORS headers to the response after handler
217
+ addCorsHeaders ( config , reqCtx ) ;
218
+ } ;
219
+ } ;
0 commit comments