serverless-offline 11.2.3 → 11.3.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/README.md CHANGED
@@ -43,7 +43,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
43
43
  - [Installation](#installation)
44
44
  - [Usage and command line options](#usage-and-command-line-options)
45
45
  - [Run modes](#run-modes)
46
- - [Usage with `invoke`](#usage-with-invoke)
46
+ - [Invoke Lambda](#invoke-lambda)
47
47
  - [The `process.env.IS_OFFLINE` variable](#the-processenvis_offline-variable)
48
48
  - [Docker and Layers](#docker-and-layers)
49
49
  - [Authorizers](#authorizers)
@@ -295,7 +295,7 @@ Lambda handlers for the `node.js` runtime can run in different execution modes w
295
295
 
296
296
  the Lambda handler process is running in a child process.
297
297
 
298
- ## Usage with `invoke`
298
+ ## Invoke Lambda
299
299
 
300
300
  To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
301
301
 
@@ -326,14 +326,20 @@ const lambda = new aws.Lambda({
326
326
  })
327
327
 
328
328
  export async function handler() {
329
- const clientContextData = stringify({ foo: 'foo' })
329
+ const clientContextData = stringify({
330
+ foo: 'foo',
331
+ })
332
+
333
+ const payload = stringify({
334
+ data: 'foo',
335
+ })
330
336
 
331
337
  const params = {
332
338
  ClientContext: Buffer.from(clientContextData).toString('base64'),
333
339
  // FunctionName is composed of: service name - stage - function name, e.g.
334
340
  FunctionName: 'myServiceName-dev-invokedHandler',
335
341
  InvocationType: 'RequestResponse',
336
- Payload: stringify({ data: 'foo' }),
342
+ Payload: payload,
337
343
  }
338
344
 
339
345
  const response = await lambda.invoke(params).promise()
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.2.3",
4
+ "version": "11.3.0",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -82,11 +82,11 @@
82
82
  ]
83
83
  },
84
84
  "dependencies": {
85
- "@aws-sdk/client-lambda": "^3.200.0",
85
+ "@aws-sdk/client-lambda": "^3.204.0",
86
86
  "@hapi/boom": "^10.0.0",
87
87
  "@hapi/h2o2": "^10.0.0",
88
- "@hapi/hapi": "^20.2.2",
89
- "@serverless/utils": "^6.8.1",
88
+ "@hapi/hapi": "^21.0.0",
89
+ "@serverless/utils": "^6.8.2",
90
90
  "boxen": "^7.0.0",
91
91
  "chalk": "^5.1.2",
92
92
  "execa": "^6.1.0",
@@ -101,16 +101,16 @@
101
101
  "luxon": "^3.1.0",
102
102
  "node-fetch": "^3.2.10",
103
103
  "node-schedule": "^2.1.0",
104
- "object.hasown": "^1.1.1",
104
+ "object.hasown": "^1.1.2",
105
105
  "p-memoize": "^7.1.1",
106
106
  "p-retry": "^5.1.1",
107
107
  "velocityjs": "^2.0.6",
108
- "ws": "^8.10.0"
108
+ "ws": "^8.11.0"
109
109
  },
110
110
  "devDependencies": {
111
111
  "@istanbuljs/esm-loader-hook": "^0.2.0",
112
112
  "archiver": "^5.3.1",
113
- "eslint": "^8.26.0",
113
+ "eslint": "^8.27.0",
114
114
  "eslint-config-airbnb-base": "^15.0.0",
115
115
  "eslint-config-prettier": "^8.5.0",
116
116
  "eslint-plugin-import": "^2.25.4",
@@ -121,7 +121,7 @@
121
121
  "mocha": "^10.1.0",
122
122
  "nyc": "^15.1.0",
123
123
  "prettier": "^2.7.1",
124
- "serverless": "^3.23.0",
124
+ "serverless": "^3.24.1",
125
125
  "standard-version": "^9.5.0"
126
126
  },
127
127
  "peerDependencies": {
@@ -253,6 +253,16 @@ export default class HttpServer {
253
253
  return null
254
254
  }
255
255
 
256
+ if (
257
+ (endpoint.authorizer.name &&
258
+ this.#serverless.service.provider?.httpApi?.authorizers?.[
259
+ endpoint.authorizer.name
260
+ ]?.type === 'request') ||
261
+ endpoint.authorizer.type === 'request'
262
+ ) {
263
+ return null
264
+ }
265
+
256
266
  const jwtSettings = this.#extractJWTAuthSettings(endpoint)
257
267
  if (!jwtSettings) {
258
268
  return null
@@ -303,11 +313,36 @@ export default class HttpServer {
303
313
  log.error(`Authorization function ${authFunctionName} does not exist`)
304
314
  return null
305
315
  }
316
+ const serverlessAuthorizerOptions =
317
+ this.#serverless.service.provider.httpApi &&
318
+ this.#serverless.service.provider.httpApi.authorizers &&
319
+ this.#serverless.service.provider.httpApi.authorizers[authFunctionName]
306
320
 
307
321
  const authorizerOptions = {
308
- identitySource: 'method.request.header.Authorization',
309
- identityValidationExpression: '(.*)',
310
- resultTtlInSeconds: '300',
322
+ enableSimpleResponses:
323
+ (endpoint.isHttpApi &&
324
+ serverlessAuthorizerOptions?.enableSimpleResponses) ||
325
+ false,
326
+ identitySource:
327
+ serverlessAuthorizerOptions?.identitySource ||
328
+ 'method.request.header.Authorization',
329
+ identityValidationExpression:
330
+ serverlessAuthorizerOptions?.identityValidationExpression || '(.*)',
331
+ payloadVersion: !endpoint.isHttpApi
332
+ ? '1.0'
333
+ : serverlessAuthorizerOptions?.payloadVersion || '2.0',
334
+ resultTtlInSeconds:
335
+ serverlessAuthorizerOptions?.resultTtlInSeconds || '300',
336
+ }
337
+
338
+ if (
339
+ authorizerOptions.enableSimpleResponses &&
340
+ authorizerOptions.payloadVersion === '1.0'
341
+ ) {
342
+ log.error(
343
+ `Cannot create Authorization function '${authFunctionName}' if payloadVersion is '1.0' and enableSimpleResponses is true`,
344
+ )
345
+ return null
311
346
  }
312
347
 
313
348
  if (typeof endpoint.authorizer === 'string') {
@@ -3,6 +3,7 @@ import { log } from '@serverless/utils/log.js'
3
3
  import authCanExecuteResource from '../authCanExecuteResource.js'
4
4
  import authValidateContext from '../authValidateContext.js'
5
5
  import {
6
+ getRawQueryParams,
6
7
  nullIfEmpty,
7
8
  parseHeaders,
8
9
  parseMultiValueHeaders,
@@ -14,18 +15,22 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
14
15
  const authFunName = authorizerOptions.name
15
16
  let identityHeader = 'authorization'
16
17
 
17
- if (authorizerOptions.type !== 'request') {
18
- const identitySourceMatch = /^method.request.header.((?:\w+-?)+\w+)$/.exec(
19
- authorizerOptions.identitySource,
20
- )
18
+ if (
19
+ authorizerOptions.type !== 'request' ||
20
+ authorizerOptions.identitySource
21
+ ) {
22
+ const identitySourceMatch =
23
+ /^(method.|\$)request.header.((?:\w+-?)+\w+)$/.exec(
24
+ authorizerOptions.identitySource,
25
+ )
21
26
 
22
- if (!identitySourceMatch || identitySourceMatch.length !== 2) {
27
+ if (!identitySourceMatch || identitySourceMatch.length !== 3) {
23
28
  throw new Error(
24
- `Serverless Offline only supports retrieving tokens from the headers (λ: ${authFunName})`,
29
+ `Serverless Offline only supports retrieving tokens from headers (λ: ${authFunName})`,
25
30
  )
26
31
  }
27
32
 
28
- identityHeader = identitySourceMatch[1].toLowerCase()
33
+ identityHeader = identitySourceMatch[2].toLowerCase()
29
34
  }
30
35
 
31
36
  // Create Auth Scheme
@@ -36,8 +41,7 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
36
41
  `Running Authorization function for ${request.method} ${request.path} (λ: ${authFunName})`,
37
42
  )
38
43
 
39
- // Get Authorization header
40
- const { req } = request.raw
44
+ const { rawHeaders, url } = request.raw.req
41
45
 
42
46
  // Get path params
43
47
  // aws doesn't auto decode path params - hapi does
@@ -45,6 +49,8 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
45
49
 
46
50
  const accountId = 'random-account-id'
47
51
  const apiId = 'random-api-id'
52
+ const requestId = 'random-request-id'
53
+
48
54
  const httpMethod = request.method.toUpperCase()
49
55
  const resourcePath = request.route.path.replace(
50
56
  new RegExp(`^/${provider.stage}`),
@@ -53,53 +59,96 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
53
59
 
54
60
  let event = {
55
61
  enhancedAuthContext: {},
56
- methodArn: `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}`,
62
+ headers: parseHeaders(rawHeaders),
57
63
  requestContext: {
58
64
  accountId,
59
65
  apiId,
60
- httpMethod,
61
- path: request.path,
62
- requestId: 'random-request-id',
63
- resourceId: 'random-resource-id',
64
- resourcePath,
66
+ domainName: `${apiId}.execute-api.us-east-1.amazonaws.com`,
67
+ domainPrefix: apiId,
68
+ requestId,
65
69
  stage: provider.stage,
66
70
  },
67
- resource: resourcePath,
71
+ version: authorizerOptions.payloadVersion,
68
72
  }
69
73
 
70
- // Create event Object for authFunction
71
- // methodArn is the ARN of the function we are running we are authorizing access to (or not)
72
- // Account ID and API ID are not simulated
73
- if (authorizerOptions.type === 'request') {
74
- const { rawHeaders, url } = req
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}"`)
75
91
 
92
+ if (authorizerOptions.payloadVersion === '1.0') {
76
93
  event = {
77
94
  ...event,
78
- headers: parseHeaders(rawHeaders),
95
+ authorizationToken: finalAuthorization,
79
96
  httpMethod: request.method.toUpperCase(),
97
+ identitySource: finalAuthorization,
98
+ methodArn,
80
99
  multiValueHeaders: parseMultiValueHeaders(rawHeaders),
81
100
  multiValueQueryStringParameters:
82
101
  parseMultiValueQueryStringParameters(url),
83
102
  path: request.path,
84
103
  pathParameters: nullIfEmpty(pathParams),
85
104
  queryStringParameters: parseQueryStringParameters(url),
86
- type: 'REQUEST',
105
+ requestContext: {
106
+ extendedRequestId: requestId,
107
+ httpMethod,
108
+ path: request.path,
109
+ protocol,
110
+ requestTime: currentDate.toString(),
111
+ requestTimeEpoch: currentDate.getTime(),
112
+ resourceId,
113
+ resourcePath,
114
+ stage: provider.stage,
115
+ },
116
+ resource: resourcePath,
87
117
  }
88
- } else {
89
- const authorization = req.headers[identityHeader]
90
-
91
- const identityValidationExpression = new RegExp(
92
- authorizerOptions.identityValidationExpression,
93
- )
94
- const matchedAuthorization =
95
- identityValidationExpression.test(authorization)
96
- const finalAuthorization = matchedAuthorization ? authorization : ''
118
+ }
97
119
 
98
- log.debug(`Retrieved ${identityHeader} header "${finalAuthorization}"`)
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,
130
+ protocol,
131
+ },
132
+ routeKey: resourceId,
133
+ time: currentDate.toString(),
134
+ timeEpoch: currentDate.getTime(),
135
+ },
136
+ routeArn: methodArn,
137
+ routeKey: resourceId,
138
+ }
139
+ }
99
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',
147
+ }
148
+ } else {
149
+ // This is safe since type: 'TOKEN' cannot have payload format 2.0
100
150
  event = {
101
151
  ...event,
102
- authorizationToken: finalAuthorization,
103
152
  type: 'TOKEN',
104
153
  }
105
154
  }
@@ -109,6 +158,25 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
109
158
 
110
159
  try {
111
160
  const result = await lambdaFunction.runHandler()
161
+
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
+ })
174
+ }
175
+ return Boom.forbidden(
176
+ 'User is not authorized to access this resource',
177
+ )
178
+ }
179
+
112
180
  if (result === 'Unauthorized') return Boom.unauthorized('Unauthorized')
113
181
 
114
182
  // Validate that the policy document has the principalId set
@@ -120,7 +188,12 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
120
188
  return Boom.forbidden('No principalId set on the Response')
121
189
  }
122
190
 
123
- if (!authCanExecuteResource(result.policyDocument, event.methodArn)) {
191
+ if (
192
+ !authCanExecuteResource(
193
+ result.policyDocument,
194
+ event.methodArn || event.routeArn,
195
+ )
196
+ ) {
124
197
  log.notice(
125
198
  `Authorization response didn't authorize user to access resource: (λ: ${authFunName})`,
126
199
  )
@@ -0,0 +1,11 @@
1
+ import parseQueryStringParameters from './parseQueryStringParameters.js'
2
+
3
+ export default function getRawQueryParams(url) {
4
+ const queryParams = parseQueryStringParameters(url) || {}
5
+ return Object.keys(queryParams)
6
+ .reduce(function reducer(accumulator, currentKey) {
7
+ accumulator.push(`${currentKey}=${queryParams[currentKey]}`)
8
+ return accumulator
9
+ }, [])
10
+ .join('&')
11
+ }
@@ -7,6 +7,7 @@ export { default as formatToClfTime } from './formatToClfTime.js'
7
7
  export { default as generateHapiPath } from './generateHapiPath.js'
8
8
  export { default as getApiKeysValues } from './getApiKeysValues.js'
9
9
  export { default as getHttpApiCorsConfig } from './getHttpApiCorsConfig.js'
10
+ export { default as getRawQueryParams } from './getRawQueryParams.js'
10
11
  export { default as jsonPath } from './jsonPath.js'
11
12
  export { default as lowerCaseKeys } from './lowerCaseKeys.js'
12
13
  export { default as parseHeaders } from './parseHeaders.js'