serverless-offline 11.3.0 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dedicatedTo": "Blue, a great migrating bird.",
3
3
  "name": "serverless-offline",
4
- "version": "11.3.0",
4
+ "version": "11.5.0",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -82,7 +82,7 @@
82
82
  ]
83
83
  },
84
84
  "dependencies": {
85
- "@aws-sdk/client-lambda": "^3.204.0",
85
+ "@aws-sdk/client-lambda": "^3.210.0",
86
86
  "@hapi/boom": "^10.0.0",
87
87
  "@hapi/h2o2": "^10.0.0",
88
88
  "@hapi/hapi": "^21.0.0",
@@ -93,13 +93,13 @@
93
93
  "fs-extra": "^10.1.0",
94
94
  "is-wsl": "^2.2.0",
95
95
  "java-invoke-local": "0.0.6",
96
- "jose": "^4.10.4",
96
+ "jose": "^4.11.0",
97
97
  "js-string-escape": "^1.0.1",
98
98
  "jsonpath-plus": "^7.2.0",
99
99
  "jsonschema": "^1.4.1",
100
100
  "jszip": "^3.10.1",
101
101
  "luxon": "^3.1.0",
102
- "node-fetch": "^3.2.10",
102
+ "node-fetch": "^3.3.0",
103
103
  "node-schedule": "^2.1.0",
104
104
  "object.hasown": "^1.1.2",
105
105
  "p-memoize": "^7.1.1",
@@ -116,7 +116,7 @@
116
116
  "eslint-plugin-import": "^2.25.4",
117
117
  "eslint-plugin-prettier": "^4.2.1",
118
118
  "git-list-updated": "^1.2.1",
119
- "husky": "^8.0.1",
119
+ "husky": "^8.0.2",
120
120
  "lint-staged": "^13.0.3",
121
121
  "mocha": "^10.1.0",
122
122
  "nyc": "^15.1.0",
@@ -18,6 +18,7 @@ export const supportedNodejs = new Set([
18
18
  'nodejs12.x',
19
19
  'nodejs14.x',
20
20
  'nodejs16.x',
21
+ 'nodejs18.x',
21
22
  ])
22
23
 
23
24
  // PROVIDED
@@ -49,5 +50,6 @@ export const supportedRuntimes = new Set([
49
50
  export const unsupportedDockerRuntimes = new Set([
50
51
  'nodejs14.x',
51
52
  'nodejs16.x',
53
+ 'nodejs18.x',
52
54
  'python3.9',
53
55
  ])
@@ -11,239 +11,287 @@ import {
11
11
  parseQueryStringParameters,
12
12
  } from '../../utils/index.js'
13
13
 
14
+ const IDENTITY_SOURCE_TYPE_HEADER = 'header'
15
+ const IDENTITY_SOURCE_TYPE_QUERYSTRING = 'querystring'
16
+
14
17
  export default function createAuthScheme(authorizerOptions, provider, lambda) {
15
18
  const authFunName = authorizerOptions.name
16
- let identityHeader = 'authorization'
19
+ let identitySourceField = 'authorization'
20
+ let identitySourceType = IDENTITY_SOURCE_TYPE_HEADER
17
21
 
18
- if (
19
- authorizerOptions.type !== 'request' ||
20
- authorizerOptions.identitySource
21
- ) {
22
- const identitySourceMatch =
23
- /^(method.|\$)request.header.((?:\w+-?)+\w+)$/.exec(
24
- authorizerOptions.identitySource,
25
- )
26
-
27
- if (!identitySourceMatch || identitySourceMatch.length !== 3) {
28
- throw new Error(
29
- `Serverless Offline only supports retrieving tokens from headers (λ: ${authFunName})`,
30
- )
31
- }
22
+ const finalizeAuthScheme = () => {
23
+ return () => ({
24
+ async authenticate(request, h) {
25
+ log.notice()
26
+ log.notice(
27
+ `Running Authorization function for ${request.method} ${request.path} (λ: ${authFunName})`,
28
+ )
32
29
 
33
- identityHeader = identitySourceMatch[2].toLowerCase()
34
- }
30
+ const { rawHeaders, url } = request.raw.req
31
+
32
+ // Get path params
33
+ // aws doesn't auto decode path params - hapi does
34
+ const pathParams = { ...request.params }
35
35
 
36
- // Create Auth Scheme
37
- return () => ({
38
- async authenticate(request, h) {
39
- log.notice()
40
- log.notice(
41
- `Running Authorization function for ${request.method} ${request.path} (λ: ${authFunName})`,
42
- )
43
-
44
- const { rawHeaders, url } = request.raw.req
45
-
46
- // Get path params
47
- // aws doesn't auto decode path params - hapi does
48
- const pathParams = { ...request.params }
49
-
50
- const accountId = 'random-account-id'
51
- const apiId = 'random-api-id'
52
- const requestId = 'random-request-id'
53
-
54
- const httpMethod = request.method.toUpperCase()
55
- const resourcePath = request.route.path.replace(
56
- new RegExp(`^/${provider.stage}`),
57
- '',
58
- )
59
-
60
- let event = {
61
- enhancedAuthContext: {},
62
- headers: parseHeaders(rawHeaders),
63
- requestContext: {
64
- accountId,
65
- apiId,
66
- domainName: `${apiId}.execute-api.us-east-1.amazonaws.com`,
67
- domainPrefix: apiId,
68
- requestId,
69
- stage: provider.stage,
70
- },
71
- version: authorizerOptions.payloadVersion,
72
- }
73
-
74
- const protocol = `${request.server.info.protocol.toUpperCase()}/${
75
- request.raw.req.httpVersion
76
- }`
77
- const currentDate = new Date()
78
- const resourceId = `${httpMethod} ${resourcePath}`
79
- const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}`
80
-
81
- const authorization = request.raw.req.headers[identityHeader]
82
-
83
- const identityValidationExpression = new RegExp(
84
- authorizerOptions.identityValidationExpression,
85
- )
86
- const matchedAuthorization =
87
- identityValidationExpression.test(authorization)
88
- const finalAuthorization = matchedAuthorization ? authorization : ''
89
-
90
- log.debug(`Retrieved ${identityHeader} header "${finalAuthorization}"`)
91
-
92
- if (authorizerOptions.payloadVersion === '1.0') {
93
- event = {
94
- ...event,
95
- authorizationToken: finalAuthorization,
96
- httpMethod: request.method.toUpperCase(),
97
- identitySource: finalAuthorization,
98
- methodArn,
99
- multiValueHeaders: parseMultiValueHeaders(rawHeaders),
100
- multiValueQueryStringParameters:
101
- parseMultiValueQueryStringParameters(url),
102
- path: request.path,
103
- pathParameters: nullIfEmpty(pathParams),
104
- queryStringParameters: parseQueryStringParameters(url),
36
+ const accountId = 'random-account-id'
37
+ const apiId = 'random-api-id'
38
+ const requestId = 'random-request-id'
39
+
40
+ const httpMethod = request.method.toUpperCase()
41
+ const resourcePath = request.route.path.replace(
42
+ new RegExp(`^/${provider.stage}`),
43
+ '',
44
+ )
45
+
46
+ let event = {
47
+ enhancedAuthContext: {},
48
+ headers: parseHeaders(rawHeaders),
105
49
  requestContext: {
106
- extendedRequestId: requestId,
107
- httpMethod,
108
- path: request.path,
109
- protocol,
110
- requestTime: currentDate.toString(),
111
- requestTimeEpoch: currentDate.getTime(),
112
- resourceId,
113
- resourcePath,
50
+ accountId,
51
+ apiId,
52
+ domainName: `${apiId}.execute-api.us-east-1.amazonaws.com`,
53
+ domainPrefix: apiId,
54
+ requestId,
114
55
  stage: provider.stage,
115
56
  },
116
- resource: resourcePath,
57
+ version: authorizerOptions.payloadVersion,
117
58
  }
118
- }
119
-
120
- if (authorizerOptions.payloadVersion === '2.0') {
121
- event = {
122
- ...event,
123
- identitySource: [finalAuthorization],
124
- rawPath: request.path,
125
- rawQueryString: getRawQueryParams(url),
126
- requestContext: {
127
- http: {
128
- method: httpMethod,
129
- path: resourcePath,
59
+
60
+ const protocol = `${request.server.info.protocol.toUpperCase()}/${
61
+ request.raw.req.httpVersion
62
+ }`
63
+ const currentDate = new Date()
64
+ const resourceId = `${httpMethod} ${resourcePath}`
65
+ const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}`
66
+
67
+ let authorization
68
+ if (identitySourceType === IDENTITY_SOURCE_TYPE_HEADER) {
69
+ const headers = request.raw.req.headers ?? {}
70
+ authorization = headers[identitySourceField]
71
+ } else if (identitySourceType === IDENTITY_SOURCE_TYPE_QUERYSTRING) {
72
+ const queryStringParameters = parseQueryStringParameters(url) ?? {}
73
+ authorization = queryStringParameters[identitySourceField]
74
+ } else {
75
+ throw new Error(
76
+ `No Authorization source has been specified. This should never happen. (λ: ${authFunName})`,
77
+ )
78
+ }
79
+
80
+ if (authorization === undefined) {
81
+ throw new Error(
82
+ `Identity Source is null for ${identitySourceType} ${identitySourceField} (λ: ${authFunName})`,
83
+ )
84
+ }
85
+
86
+ const identityValidationExpression = new RegExp(
87
+ authorizerOptions.identityValidationExpression,
88
+ )
89
+ const matchedAuthorization =
90
+ identityValidationExpression.test(authorization)
91
+ const finalAuthorization = matchedAuthorization ? authorization : ''
92
+
93
+ log.debug(
94
+ `Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`,
95
+ )
96
+
97
+ if (authorizerOptions.payloadVersion === '1.0') {
98
+ event = {
99
+ ...event,
100
+ authorizationToken: finalAuthorization,
101
+ httpMethod: request.method.toUpperCase(),
102
+ identitySource: finalAuthorization,
103
+ methodArn,
104
+ multiValueHeaders: parseMultiValueHeaders(rawHeaders),
105
+ multiValueQueryStringParameters:
106
+ parseMultiValueQueryStringParameters(url),
107
+ path: request.path,
108
+ pathParameters: nullIfEmpty(pathParams),
109
+ queryStringParameters: parseQueryStringParameters(url),
110
+ requestContext: {
111
+ extendedRequestId: requestId,
112
+ httpMethod,
113
+ path: request.path,
130
114
  protocol,
115
+ requestTime: currentDate.toString(),
116
+ requestTimeEpoch: currentDate.getTime(),
117
+ resourceId,
118
+ resourcePath,
119
+ stage: provider.stage,
131
120
  },
132
- routeKey: resourceId,
133
- time: currentDate.toString(),
134
- timeEpoch: currentDate.getTime(),
135
- },
136
- routeArn: methodArn,
137
- routeKey: resourceId,
121
+ resource: resourcePath,
122
+ }
138
123
  }
139
- }
140
-
141
- // methodArn is the ARN of the function we are running we are authorizing access to (or not)
142
- // Account ID and API ID are not simulated
143
- if (authorizerOptions.type === 'request') {
144
- event = {
145
- ...event,
146
- type: 'REQUEST',
124
+
125
+ if (authorizerOptions.payloadVersion === '2.0') {
126
+ event = {
127
+ ...event,
128
+ identitySource: [finalAuthorization],
129
+ rawPath: request.path,
130
+ rawQueryString: getRawQueryParams(url),
131
+ requestContext: {
132
+ http: {
133
+ method: httpMethod,
134
+ path: resourcePath,
135
+ protocol,
136
+ },
137
+ routeKey: resourceId,
138
+ time: currentDate.toString(),
139
+ timeEpoch: currentDate.getTime(),
140
+ },
141
+ routeArn: methodArn,
142
+ routeKey: resourceId,
143
+ }
147
144
  }
148
- } else {
149
- // This is safe since type: 'TOKEN' cannot have payload format 2.0
150
- event = {
151
- ...event,
152
- type: 'TOKEN',
145
+
146
+ // methodArn is the ARN of the function we are running we are authorizing access to (or not)
147
+ // Account ID and API ID are not simulated
148
+ if (authorizerOptions.type === 'request') {
149
+ event = {
150
+ ...event,
151
+ type: 'REQUEST',
152
+ }
153
+ } else {
154
+ // This is safe since type: 'TOKEN' cannot have payload format 2.0
155
+ event = {
156
+ ...event,
157
+ type: 'TOKEN',
158
+ }
153
159
  }
154
- }
155
160
 
156
- const lambdaFunction = lambda.get(authFunName)
157
- lambdaFunction.setEvent(event)
161
+ const lambdaFunction = lambda.get(authFunName)
162
+ lambdaFunction.setEvent(event)
163
+
164
+ try {
165
+ const result = await lambdaFunction.runHandler()
166
+
167
+ if (authorizerOptions.enableSimpleResponses) {
168
+ if (result.isAuthorized) {
169
+ const authorizer = {
170
+ integrationLatency: '42',
171
+ ...result.context,
172
+ }
173
+ return h.authenticated({
174
+ credentials: {
175
+ authorizer,
176
+ context: result.context || {},
177
+ },
178
+ })
179
+ }
180
+ return Boom.forbidden(
181
+ 'User is not authorized to access this resource',
182
+ )
183
+ }
184
+
185
+ if (result === 'Unauthorized')
186
+ return Boom.unauthorized('Unauthorized')
158
187
 
159
- try {
160
- const result = await lambdaFunction.runHandler()
188
+ // Validate that the policy document has the principalId set
189
+ if (!result.principalId) {
190
+ log.notice(
191
+ `Authorization response did not include a principalId: (λ: ${authFunName})`,
192
+ )
161
193
 
162
- if (authorizerOptions.enableSimpleResponses) {
163
- if (result.isAuthorized) {
164
- const authorizer = {
165
- integrationLatency: '42',
166
- ...result.context,
167
- }
168
- return h.authenticated({
169
- credentials: {
170
- authorizer,
171
- context: result.context || {},
172
- },
173
- })
194
+ return Boom.forbidden('No principalId set on the Response')
174
195
  }
175
- return Boom.forbidden(
176
- 'User is not authorized to access this resource',
177
- )
178
- }
179
196
 
180
- if (result === 'Unauthorized') return Boom.unauthorized('Unauthorized')
197
+ if (
198
+ !authCanExecuteResource(
199
+ result.policyDocument,
200
+ event.methodArn || event.routeArn,
201
+ )
202
+ ) {
203
+ log.notice(
204
+ `Authorization response didn't authorize user to access resource: (λ: ${authFunName})`,
205
+ )
206
+
207
+ return Boom.forbidden(
208
+ 'User is not authorized to access this resource',
209
+ )
210
+ }
211
+
212
+ // validate the resulting context, ensuring that all
213
+ // values are either string, number, or boolean types
214
+ if (result.context) {
215
+ const validationResult = authValidateContext(
216
+ result.context,
217
+ authFunName,
218
+ )
219
+
220
+ if (validationResult instanceof Error) {
221
+ return validationResult
222
+ }
223
+
224
+ result.context = validationResult
225
+ }
181
226
 
182
- // Validate that the policy document has the principalId set
183
- if (!result.principalId) {
184
227
  log.notice(
185
- `Authorization response did not include a principalId: (λ: ${authFunName})`,
228
+ `Authorization function returned a successful response: (λ: ${authFunName})`,
186
229
  )
187
230
 
188
- return Boom.forbidden('No principalId set on the Response')
189
- }
231
+ const authorizer = {
232
+ integrationLatency: '42',
233
+ principalId: result.principalId,
234
+ ...result.context,
235
+ }
190
236
 
191
- if (
192
- !authCanExecuteResource(
193
- result.policyDocument,
194
- event.methodArn || event.routeArn,
195
- )
196
- ) {
237
+ // Set the credentials for the rest of the pipeline
238
+ return h.authenticated({
239
+ credentials: {
240
+ authorizer,
241
+ context: result.context,
242
+ principalId: result.principalId,
243
+ usageIdentifierKey: result.usageIdentifierKey,
244
+ },
245
+ })
246
+ } catch {
197
247
  log.notice(
198
- `Authorization response didn't authorize user to access resource: (λ: ${authFunName})`,
248
+ `Authorization function returned an error response: (λ: ${authFunName})`,
199
249
  )
200
250
 
201
- return Boom.forbidden(
202
- 'User is not authorized to access this resource',
203
- )
251
+ return Boom.unauthorized('Unauthorized')
204
252
  }
253
+ },
254
+ })
255
+ }
205
256
 
206
- // validate the resulting context, ensuring that all
207
- // values are either string, number, or boolean types
208
- if (result.context) {
209
- const validationResult = authValidateContext(
210
- result.context,
211
- authFunName,
212
- )
213
-
214
- if (validationResult instanceof Error) {
215
- return validationResult
216
- }
257
+ const checkForIdentitySourceMatch = (exp, expectedLength) => {
258
+ const identitySourceMatch = exp.exec(authorizerOptions.identitySource)
217
259
 
218
- result.context = validationResult
219
- }
260
+ if (!identitySourceMatch || identitySourceMatch.length !== expectedLength) {
261
+ return undefined
262
+ }
263
+ return identitySourceMatch[expectedLength - 1]
264
+ }
220
265
 
221
- log.notice(
222
- `Authorization function returned a successful response: (λ: ${authFunName})`,
223
- )
266
+ if (
267
+ authorizerOptions.type !== 'request' ||
268
+ authorizerOptions.identitySource
269
+ ) {
270
+ const headerRegExp = /^(method.|\$)request.header.((?:\w+-?)+\w+)$/
271
+ const queryStringRegExp =
272
+ /^(method.|\$)request.querystring.((?:\w+-?)+\w+)$/
273
+
274
+ const identityHeaderResult = checkForIdentitySourceMatch(headerRegExp, 3)
275
+ if (identityHeaderResult !== undefined) {
276
+ identitySourceField = identityHeaderResult.toLowerCase()
277
+ identitySourceType = IDENTITY_SOURCE_TYPE_HEADER
278
+ return finalizeAuthScheme()
279
+ }
224
280
 
225
- const authorizer = {
226
- integrationLatency: '42',
227
- principalId: result.principalId,
228
- ...result.context,
229
- }
281
+ const identityQueryStringResult = checkForIdentitySourceMatch(
282
+ queryStringRegExp,
283
+ 3,
284
+ )
285
+ if (identityQueryStringResult !== undefined) {
286
+ identitySourceField = identityQueryStringResult
287
+ identitySourceType = IDENTITY_SOURCE_TYPE_QUERYSTRING
288
+ return finalizeAuthScheme()
289
+ }
230
290
 
231
- // Set the credentials for the rest of the pipeline
232
- return h.authenticated({
233
- credentials: {
234
- authorizer,
235
- context: result.context,
236
- principalId: result.principalId,
237
- usageIdentifierKey: result.usageIdentifierKey,
238
- },
239
- })
240
- } catch {
241
- log.notice(
242
- `Authorization function returned an error response: (λ: ${authFunName})`,
243
- )
291
+ throw new Error(
292
+ `Serverless Offline only supports retrieving tokens from headers and querystring parameters (λ: ${authFunName})`,
293
+ )
294
+ }
244
295
 
245
- return Boom.unauthorized('Unauthorized')
246
- }
247
- },
248
- })
296
+ return finalizeAuthScheme()
249
297
  }