serverless-offline 9.1.2 → 9.1.5

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": "9.1.2",
4
+ "version": "9.1.5",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.js",
@@ -194,7 +194,8 @@
194
194
  "@hapi/boom": "^10.0.0",
195
195
  "@hapi/h2o2": "^9.1.0",
196
196
  "@hapi/hapi": "^20.2.2",
197
- "aws-sdk": "^2.1185.0",
197
+ "@serverless/utils": "^6.7.0",
198
+ "aws-sdk": "^2.1187.0",
198
199
  "boxen": "^7.0.0",
199
200
  "chalk": "^5.0.1",
200
201
  "execa": "^6.1.0",
@@ -204,7 +205,7 @@
204
205
  "jsonpath-plus": "^7.0.0",
205
206
  "jsonschema": "^1.4.1",
206
207
  "jsonwebtoken": "^8.5.1",
207
- "jszip": "^3.10.0",
208
+ "jszip": "^3.10.1",
208
209
  "luxon": "^3.0.1",
209
210
  "node-fetch": "^3.2.10",
210
211
  "node-schedule": "^2.1.0",
@@ -216,7 +217,7 @@
216
217
  },
217
218
  "devDependencies": {
218
219
  "archiver": "^5.3.1",
219
- "eslint": "^8.20.0",
220
+ "eslint": "^8.21.0",
220
221
  "eslint-config-airbnb-base": "^15.0.0",
221
222
  "eslint-config-prettier": "^8.5.0",
222
223
  "eslint-plugin-import": "^2.25.4",
@@ -230,7 +231,6 @@
230
231
  "standard-version": "^9.5.0"
231
232
  },
232
233
  "peerDependencies": {
233
- "@serverless/utils": "^6.7.0",
234
234
  "serverless": "^3.2.0"
235
235
  }
236
236
  }
@@ -168,7 +168,7 @@ export default class ServerlessOffline {
168
168
 
169
169
  this.#http = new Http(this.#serverless, this.#options, this.#lambda)
170
170
 
171
- await this.#http.registerPlugins()
171
+ await this.#http.createServer()
172
172
 
173
173
  this.#http.create(events)
174
174
 
@@ -236,12 +236,8 @@ export default class ServerlessOffline {
236
236
  .replace(/\s/g, '')
237
237
  .split(',')
238
238
 
239
- if (this.#options.corsDisallowCredentials) {
240
- this.#options.corsAllowCredentials = false
241
- }
242
-
243
239
  this.#options.corsConfig = {
244
- credentials: this.#options.corsAllowCredentials,
240
+ credentials: !this.#options.corsDisallowCredentials,
245
241
  exposedHeaders: this.#options.corsExposedHeaders,
246
242
  headers: this.#options.corsAllowHeaders,
247
243
  origin: this.#options.corsAllowOrigin,
@@ -2,9 +2,9 @@ import { createApiKey } from '../utils/index.js'
2
2
 
3
3
  export default {
4
4
  apiKey: createApiKey(),
5
- corsAllowCredentials: true, // TODO no CLI option
6
5
  corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
7
6
  corsAllowOrigin: '*',
7
+ corsDisallowCredentials: true,
8
8
  corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
9
9
  disableCookieValidation: false,
10
10
  disableScheduledEvents: false,
@@ -17,6 +17,10 @@ export default class Http {
17
17
  return this.#httpServer.stop(timeout)
18
18
  }
19
19
 
20
+ async createServer() {
21
+ await this.#httpServer.createServer()
22
+ }
23
+
20
24
  #createEvent(functionKey, rawHttpEventDefinition, handler) {
21
25
  const httpEvent = new HttpEventDefinition(rawHttpEventDefinition)
22
26
 
@@ -39,10 +43,6 @@ export default class Http {
39
43
  this.#httpServer.create404Route()
40
44
  }
41
45
 
42
- registerPlugins() {
43
- return this.#httpServer.registerPlugins()
44
- }
45
-
46
46
  // TEMP FIXME quick fix to expose gateway server for testing, look for better solution
47
47
  getServer() {
48
48
  return this.#httpServer.getServer()
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import { readFileSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
3
  import { createRequire } from 'node:module'
4
4
  import { join, resolve } from 'node:path'
5
5
  import { exit } from 'node:process'
@@ -47,7 +47,9 @@ export default class HttpServer {
47
47
  this.#lambda = lambda
48
48
  this.#options = options
49
49
  this.#serverless = serverless
50
+ }
50
51
 
52
+ async createServer() {
51
53
  const {
52
54
  enforceSecureCookies,
53
55
  host,
@@ -80,14 +82,20 @@ export default class HttpServer {
80
82
  // HTTPS support
81
83
  if (typeof httpsProtocol === 'string' && httpsProtocol.length > 0) {
82
84
  serverOptions.tls = {
83
- cert: readFileSync(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
84
- key: readFileSync(resolve(httpsProtocol, 'key.pem'), 'ascii'),
85
+ cert: readFile(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
86
+ key: readFile(resolve(httpsProtocol, 'key.pem'), 'ascii'),
85
87
  }
86
88
  }
87
89
 
88
90
  // Hapijs server creation
89
91
  this.#server = new Server(serverOptions)
90
92
 
93
+ try {
94
+ await this.#server.register([h2o2])
95
+ } catch (err) {
96
+ log.error(err)
97
+ }
98
+
91
99
  // Enable CORS preflight response
92
100
  this.#server.ext('onPreResponse', (request, h) => {
93
101
  if (request.headers.origin) {
@@ -205,14 +213,6 @@ export default class HttpServer {
205
213
  })
206
214
  }
207
215
 
208
- async registerPlugins() {
209
- try {
210
- await this.#server.register([h2o2])
211
- } catch (err) {
212
- log.error(err)
213
- }
214
- }
215
-
216
216
  #logPluginIssue() {
217
217
  log.notice(
218
218
  'If you think this is an issue with the plugin please submit it, thanks!\nhttps://github.com/dherault/serverless-offline/issues',
@@ -260,7 +260,7 @@ export default class HttpServer {
260
260
  log.debug(`Creating Authorization scheme for ${authKey}`)
261
261
 
262
262
  // Create the Auth Scheme for the endpoint
263
- const scheme = createJWTAuthScheme(jwtSettings, this)
263
+ const scheme = createJWTAuthScheme(jwtSettings)
264
264
 
265
265
  // Set the auth scheme and strategy on the server
266
266
  this.#server.auth.scheme(authSchemeName, scheme)
@@ -338,6 +338,7 @@ export default class HttpServer {
338
338
  * /tests/integration/custom-authentication
339
339
  */
340
340
  const customizations = this.#serverless.service.custom
341
+
341
342
  if (
342
343
  customizations &&
343
344
  customizations.offline?.customAuthenticationProvider
@@ -350,11 +351,13 @@ export default class HttpServer {
350
351
  )
351
352
 
352
353
  const strategy = provider(endpoint, functionKey, method, path)
354
+
353
355
  this.#server.auth.scheme(
354
356
  strategy.scheme,
355
357
  strategy.getAuthenticateFunction,
356
358
  )
357
359
  this.#server.auth.strategy(strategy.name, strategy.scheme)
360
+
358
361
  return strategy.name
359
362
  }
360
363
 
@@ -363,164 +366,44 @@ export default class HttpServer {
363
366
  ? null
364
367
  : this.#configureJWTAuthorization(endpoint, functionKey, method, path) ||
365
368
  this.#configureAuthorization(endpoint, functionKey, method, path)
369
+
366
370
  return authStrategyName
367
371
  }
368
372
 
369
- createRoutes(functionKey, httpEvent, handler) {
370
- const [handlerPath] = splitHandlerPathAndName(handler)
371
-
372
- let method
373
- let path
374
- let hapiPath
375
-
376
- if (httpEvent.isHttpApi) {
377
- if (httpEvent.routeKey === '$default') {
378
- method = 'ANY'
379
- path = httpEvent.routeKey
380
- hapiPath = '/{default*}'
381
- } else {
382
- ;[method, path] = httpEvent.routeKey.split(' ')
383
- hapiPath = generateHapiPath(
384
- path,
385
- {
386
- ...this.#options,
387
- noPrependStageInUrl: true, // Serverless always uses the $default stage
388
- },
389
- this.#serverless,
390
- )
391
- }
392
- } else {
393
- method = httpEvent.method.toUpperCase()
394
- ;({ path } = httpEvent)
395
- hapiPath = generateHapiPath(path, this.#options, this.#serverless)
396
- }
397
-
398
- const endpoint = new Endpoint(
399
- join(this.#serverless.config.servicePath, handlerPath),
400
- httpEvent,
401
- ).generate()
402
-
403
- const stage = endpoint.isHttpApi
404
- ? '$default'
405
- : this.#options.stage || this.#serverless.service.provider.stage
406
- const protectedRoutes = []
407
-
408
- if (httpEvent.private) {
409
- protectedRoutes.push(`${method}#${hapiPath}`)
410
- }
411
-
412
- const { host, httpPort, httpsProtocol } = this.#options
413
- const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
414
-
415
- this.#terminalInfo.push({
416
- invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
417
- method,
418
- path: hapiPath,
419
- server,
420
- stage:
421
- endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
422
- })
423
-
424
- const authStrategyName = this.#setAuthorizationStrategy(
373
+ #createHapiHandler(params) {
374
+ const {
375
+ additionalRequestContext,
425
376
  endpoint,
426
377
  functionKey,
378
+ hapiMethod,
379
+ hapiPath,
427
380
  method,
428
- path,
429
- )
430
-
431
- let cors = null
432
- if (endpoint.cors) {
433
- cors = {
434
- credentials:
435
- endpoint.cors.credentials || this.#options.corsConfig.credentials,
436
- exposedHeaders: this.#options.corsConfig.exposedHeaders,
437
- headers: endpoint.cors.headers || this.#options.corsConfig.headers,
438
- origin: endpoint.cors.origins || this.#options.corsConfig.origin,
439
- }
440
- } else if (
441
- this.#serverless.service.provider.httpApi &&
442
- this.#serverless.service.provider.httpApi.cors
443
- ) {
444
- const httpApiCors = getHttpApiCorsConfig(
445
- this.#serverless.service.provider.httpApi.cors,
446
- this,
447
- )
448
- cors = {
449
- credentials: httpApiCors.allowCredentials,
450
- exposedHeaders: httpApiCors.exposedResponseHeaders || [],
451
- headers: httpApiCors.allowedHeaders || [],
452
- maxAge: httpApiCors.maxAge,
453
- origin: httpApiCors.allowedOrigins || [],
454
- }
455
- }
456
-
457
- const hapiMethod = method === 'ANY' ? '*' : method
458
-
459
- const state = this.#options.disableCookieValidation
460
- ? {
461
- failAction: 'ignore',
462
- parse: false,
463
- }
464
- : {
465
- failAction: 'error',
466
- parse: true,
467
- }
468
-
469
- const hapiOptions = {
470
- auth: authStrategyName,
471
- cors,
472
- state,
473
- timeout: { socket: false },
474
- }
475
-
476
- // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
477
- // for more details, check https://github.com/dherault/serverless-offline/issues/204
478
- if (hapiMethod === 'HEAD') {
479
- log.notice(
480
- 'HEAD method event detected. Skipping HAPI server route mapping',
481
- )
482
-
483
- return
484
- }
485
-
486
- if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
487
- // maxBytes: Increase request size from 1MB default limit to 10MB.
488
- // Cf AWS API GW payload limits.
489
- hapiOptions.payload = {
490
- maxBytes: 1024 * 1024 * 10,
491
- parse: false,
492
- }
493
- }
494
-
495
- const additionalRequestContext = {}
496
- if (httpEvent.operationId) {
497
- additionalRequestContext.operationName = httpEvent.operationId
498
- }
499
-
500
- hapiOptions.tags = ['api']
381
+ protectedRoute,
382
+ stage,
383
+ } = params
501
384
 
502
- const hapiHandler = async (request, h) => {
385
+ return async (request, h) => {
503
386
  const requestPath =
504
387
  endpoint.isHttpApi || this.#options.noPrependStageInUrl
505
388
  ? request.path
506
389
  : request.path.substr(`/${stage}`.length)
507
390
 
508
- // Payload processing
391
+ // payload processing
509
392
  const encoding = detectEncoding(request)
510
393
 
511
394
  request.payload = request.payload && request.payload.toString(encoding)
512
395
  request.rawPayload = request.payload
513
396
 
514
- // Incomming request message
397
+ // incomming request message
515
398
  log.notice()
516
399
 
517
400
  log.notice()
518
401
  log.notice(`${method} ${request.path} (λ: ${functionKey})`)
519
402
 
520
- // Check for APIKey
403
+ // check for APIKey
521
404
  if (
522
- (protectedRoutes.includes(`${hapiMethod}#${hapiPath}`) ||
523
- protectedRoutes.includes(`ANY#${hapiPath}`)) &&
405
+ (protectedRoute === `${hapiMethod}#${hapiPath}` ||
406
+ protectedRoute === `ANY#${hapiPath}`) &&
524
407
  !this.#options.noAuth
525
408
  ) {
526
409
  const errorResponse = () =>
@@ -704,8 +587,7 @@ export default class HttpServer {
704
587
 
705
588
  const errorMessage = (err.message || err).toString()
706
589
 
707
- const re = /\[(\d{3})]/
708
- const found = errorMessage.match(re)
590
+ const found = errorMessage.match(/\[(\d{3})]/)
709
591
 
710
592
  if (found && found.length > 1) {
711
593
  ;[, errorStatusCode] = found
@@ -856,7 +738,9 @@ export default class HttpServer {
856
738
  ).getContext()
857
739
 
858
740
  result = renderVelocityTemplateObject(
859
- { root: responseTemplate },
741
+ {
742
+ root: responseTemplate,
743
+ },
860
744
  reponseContext,
861
745
  ).root
862
746
  } catch (error) {
@@ -965,7 +849,9 @@ export default class HttpServer {
965
849
  headerValue.forEach((value) => {
966
850
  // it looks like Hapi doesn't support multiple headers with the same name,
967
851
  // appending values is the closest we can come to the AWS behavior.
968
- response.header(headerKey, value, { append: true })
852
+ response.header(headerKey, value, {
853
+ append: true,
854
+ })
969
855
  })
970
856
  }
971
857
  })
@@ -1003,7 +889,6 @@ export default class HttpServer {
1003
889
  }
1004
890
  }
1005
891
 
1006
- // Log response
1007
892
  let whatToLog = result
1008
893
 
1009
894
  try {
@@ -1018,9 +903,152 @@ export default class HttpServer {
1018
903
  }
1019
904
  }
1020
905
 
1021
- // Bon voyage!
1022
906
  return response
1023
907
  }
908
+ }
909
+
910
+ createRoutes(functionKey, httpEvent, handler) {
911
+ const [handlerPath] = splitHandlerPathAndName(handler)
912
+
913
+ let method
914
+ let path
915
+ let hapiPath
916
+
917
+ if (httpEvent.isHttpApi) {
918
+ if (httpEvent.routeKey === '$default') {
919
+ method = 'ANY'
920
+ path = httpEvent.routeKey
921
+ hapiPath = '/{default*}'
922
+ } else {
923
+ ;[method, path] = httpEvent.routeKey.split(' ')
924
+ hapiPath = generateHapiPath(
925
+ path,
926
+ {
927
+ ...this.#options,
928
+ noPrependStageInUrl: true, // Serverless always uses the $default stage
929
+ },
930
+ this.#serverless,
931
+ )
932
+ }
933
+ } else {
934
+ method = httpEvent.method.toUpperCase()
935
+ ;({ path } = httpEvent)
936
+ hapiPath = generateHapiPath(path, this.#options, this.#serverless)
937
+ }
938
+
939
+ const endpoint = new Endpoint(
940
+ join(this.#serverless.config.servicePath, handlerPath),
941
+ httpEvent,
942
+ ).generate()
943
+
944
+ const stage = endpoint.isHttpApi
945
+ ? '$default'
946
+ : this.#options.stage || this.#serverless.service.provider.stage
947
+
948
+ const protectedRoute = httpEvent.private
949
+ ? `${method}#${hapiPath}`
950
+ : undefined
951
+
952
+ const { host, httpPort, httpsProtocol } = this.#options
953
+ const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
954
+
955
+ this.#terminalInfo.push({
956
+ invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
957
+ method,
958
+ path: hapiPath,
959
+ server,
960
+ stage:
961
+ endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
962
+ })
963
+
964
+ const authStrategyName = this.#setAuthorizationStrategy(
965
+ endpoint,
966
+ functionKey,
967
+ method,
968
+ path,
969
+ )
970
+
971
+ let cors = null
972
+ if (endpoint.cors) {
973
+ cors = {
974
+ credentials:
975
+ endpoint.cors.credentials || this.#options.corsConfig.credentials,
976
+ exposedHeaders: this.#options.corsConfig.exposedHeaders,
977
+ headers: endpoint.cors.headers || this.#options.corsConfig.headers,
978
+ origin: endpoint.cors.origins || this.#options.corsConfig.origin,
979
+ }
980
+ } else if (
981
+ this.#serverless.service.provider.httpApi &&
982
+ this.#serverless.service.provider.httpApi.cors
983
+ ) {
984
+ const httpApiCors = getHttpApiCorsConfig(
985
+ this.#serverless.service.provider.httpApi.cors,
986
+ this,
987
+ )
988
+ cors = {
989
+ credentials: httpApiCors.allowCredentials,
990
+ exposedHeaders: httpApiCors.exposedResponseHeaders || [],
991
+ headers: httpApiCors.allowedHeaders || [],
992
+ maxAge: httpApiCors.maxAge,
993
+ origin: httpApiCors.allowedOrigins || [],
994
+ }
995
+ }
996
+
997
+ const hapiMethod = method === 'ANY' ? '*' : method
998
+
999
+ const state = this.#options.disableCookieValidation
1000
+ ? {
1001
+ failAction: 'ignore',
1002
+ parse: false,
1003
+ }
1004
+ : {
1005
+ failAction: 'error',
1006
+ parse: true,
1007
+ }
1008
+
1009
+ const hapiOptions = {
1010
+ auth: authStrategyName,
1011
+ cors,
1012
+ state,
1013
+ timeout: { socket: false },
1014
+ }
1015
+
1016
+ // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
1017
+ // for more details, check https://github.com/dherault/serverless-offline/issues/204
1018
+ if (hapiMethod === 'HEAD') {
1019
+ log.notice(
1020
+ 'HEAD method event detected. Skipping HAPI server route mapping',
1021
+ )
1022
+
1023
+ return
1024
+ }
1025
+
1026
+ if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
1027
+ // maxBytes: Increase request size from 1MB default limit to 10MB.
1028
+ // Cf AWS API GW payload limits.
1029
+ hapiOptions.payload = {
1030
+ maxBytes: 1024 * 1024 * 10,
1031
+ parse: false,
1032
+ }
1033
+ }
1034
+
1035
+ const additionalRequestContext = {}
1036
+ if (httpEvent.operationId) {
1037
+ additionalRequestContext.operationName = httpEvent.operationId
1038
+ }
1039
+
1040
+ hapiOptions.tags = ['api']
1041
+
1042
+ const hapiHandler = this.#createHapiHandler({
1043
+ additionalRequestContext,
1044
+ endpoint,
1045
+ functionKey,
1046
+ hapiMethod,
1047
+ hapiPath,
1048
+ method,
1049
+ protectedRoute,
1050
+ stage,
1051
+ })
1024
1052
 
1025
1053
  this.#server.route({
1026
1054
  handler: hapiHandler,
@@ -41,19 +41,6 @@ export default class ChildProcessRunner {
41
41
  },
42
42
  )
43
43
 
44
- let message
45
-
46
- try {
47
- message = new Promise((res, rej) => {
48
- childProcess.on('message', (data) => {
49
- if (data.error) rej(data.error)
50
- else res(data)
51
- })
52
- })
53
- } finally {
54
- childProcess.kill()
55
- }
56
-
57
44
  childProcess.send({
58
45
  context,
59
46
  event,
@@ -63,12 +50,21 @@ export default class ChildProcessRunner {
63
50
  let result
64
51
 
65
52
  try {
66
- result = await message
53
+ result = await new Promise((res, rej) => {
54
+ childProcess.on('message', (data) => {
55
+ if (data.error) {
56
+ rej(data.error)
57
+ return
58
+ }
59
+ res(data)
60
+ })
61
+ })
67
62
  } catch (err) {
68
63
  // TODO
69
64
  log.error(err)
70
-
71
65
  throw err
66
+ } finally {
67
+ childProcess.kill()
72
68
  }
73
69
 
74
70
  return result
@@ -50,7 +50,6 @@ export default class InProcessRunner {
50
50
  let handler
51
51
 
52
52
  try {
53
- // const { [this.#handlerName]: handler } = await import(this.#handlerPath)
54
53
  // eslint-disable-next-line import/no-dynamic-require
55
54
  ;({ [this.#handlerName]: handler } = require(this.#handlerPath))
56
55
  } catch (err) {
@@ -87,15 +86,21 @@ export default class InProcessRunner {
87
86
  // create new immutable object
88
87
  const lambdaContext = {
89
88
  ...context,
90
- done: (err, data) => callback(err, data),
91
- fail: (err) => callback(err),
89
+ done(err, data) {
90
+ callback(err, data)
91
+ },
92
+ fail(err) {
93
+ callback(err)
94
+ },
92
95
  getRemainingTimeInMillis() {
93
96
  const timeLeft = executionTimeout - performance.now()
94
97
 
95
98
  // just return 0 for now if we are beyond alotted time (timeout)
96
99
  return timeLeft > 0 ? floor(timeLeft) : 0
97
100
  },
98
- succeed: (res) => callback(null, res),
101
+ succeed(res) {
102
+ callback(null, res)
103
+ },
99
104
  }
100
105
 
101
106
  let result