serverless-offline 11.2.2 → 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.2",
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,34 +82,35 @@
82
82
  ]
83
83
  },
84
84
  "dependencies": {
85
- "@aws-sdk/client-lambda": "^3.199.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",
93
93
  "fs-extra": "^10.1.0",
94
+ "is-wsl": "^2.2.0",
94
95
  "java-invoke-local": "0.0.6",
95
96
  "jose": "^4.10.4",
96
97
  "js-string-escape": "^1.0.1",
97
98
  "jsonpath-plus": "^7.2.0",
98
99
  "jsonschema": "^1.4.1",
99
100
  "jszip": "^3.10.1",
100
- "luxon": "^3.0.4",
101
+ "luxon": "^3.1.0",
101
102
  "node-fetch": "^3.2.10",
102
103
  "node-schedule": "^2.1.0",
103
- "object.hasown": "^1.1.1",
104
+ "object.hasown": "^1.1.2",
104
105
  "p-memoize": "^7.1.1",
105
106
  "p-retry": "^5.1.1",
106
107
  "velocityjs": "^2.0.6",
107
- "ws": "^8.10.0"
108
+ "ws": "^8.11.0"
108
109
  },
109
110
  "devDependencies": {
110
111
  "@istanbuljs/esm-loader-hook": "^0.2.0",
111
112
  "archiver": "^5.3.1",
112
- "eslint": "^8.26.0",
113
+ "eslint": "^8.27.0",
113
114
  "eslint-config-airbnb-base": "^15.0.0",
114
115
  "eslint-config-prettier": "^8.5.0",
115
116
  "eslint-plugin-import": "^2.25.4",
@@ -120,7 +121,7 @@
120
121
  "mocha": "^10.1.0",
121
122
  "nyc": "^15.1.0",
122
123
  "prettier": "^2.7.1",
123
- "serverless": "^3.23.0",
124
+ "serverless": "^3.24.1",
124
125
  "standard-version": "^9.5.0"
125
126
  },
126
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
  )
@@ -3,6 +3,7 @@ import { log } from '@serverless/utils/log.js'
3
3
  import { decodeJwt } from 'jose'
4
4
 
5
5
  const { isArray } = Array
6
+ const { now } = Date
6
7
 
7
8
  export default function createAuthScheme(jwtOptions) {
8
9
  const authorizerName = jwtOptions.name
@@ -38,7 +39,7 @@ export default function createAuthScheme(jwtOptions) {
38
39
  const claims = decodeJwt(jwtToken)
39
40
 
40
41
  const expirationDate = new Date(claims.exp * 1000)
41
- if (expirationDate.valueOf() < Date.now()) {
42
+ if (expirationDate.getTime() < now()) {
42
43
  return Boom.unauthorized('JWT Token expired')
43
44
  }
44
45
 
@@ -7,6 +7,7 @@ import { LambdaClient, GetLayerVersionCommand } from '@aws-sdk/client-lambda'
7
7
  import { log, progress } from '@serverless/utils/log.js'
8
8
  import { execa } from 'execa'
9
9
  import { ensureDir, pathExists } from 'fs-extra'
10
+ import isWsl from 'is-wsl'
10
11
  import jszip from 'jszip'
11
12
  import pRetry from 'p-retry'
12
13
  import DockerImage from './DockerImage.js'
@@ -148,7 +149,7 @@ export default class DockerContainer {
148
149
  dockerArgs.push('-e', `${key}=${value}`)
149
150
  })
150
151
 
151
- if (platform() === 'linux') {
152
+ if (platform() === 'linux' && !isWsl) {
152
153
  // Add `host.docker.internal` DNS name to access host from inside the container
153
154
  // https://github.com/docker/for-linux/issues/264
154
155
  const gatewayIp = await this.#getBridgeGatewayIp()
@@ -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'