serverless-offline 12.0.3 → 13.0.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 +6 -0
- package/package.json +31 -35
- package/src/ServerlessOffline.js +7 -5
- package/src/config/supportedRuntimes.js +4 -5
- package/src/events/alb/Alb.js +0 -6
- package/src/events/alb/HttpServer.js +3 -7
- package/src/events/authMatchPolicyResource.js +1 -1
- package/src/events/http/Endpoint.js +4 -7
- package/src/events/http/HttpServer.js +29 -28
- package/src/events/http/createAuthScheme.js +47 -36
- package/src/events/http/createJWTAuthScheme.js +1 -1
- package/src/events/http/javaHelpers.js +0 -7
- package/src/events/http/lambda-events/VelocityContext.js +2 -2
- package/src/events/http/lambda-events/renderVelocityTemplateObject.js +11 -6
- package/src/events/schedule/Schedule.js +8 -4
- package/src/events/schedule/ScheduleEvent.js +1 -1
- package/src/events/websocket/HttpServer.js +2 -2
- package/src/events/websocket/http-routes/connections/ConnectionsController.js +1 -1
- package/src/index.js +0 -11
- package/src/lambda/LambdaFunction.js +1 -1
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +8 -8
- package/src/lambda/handler-runner/go-runner/GoRunner.js +2 -2
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +4 -6
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +3 -5
- package/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +14 -15
- package/src/lambda/routes/invocations/InvocationsController.js +1 -1
- package/src/lambda/routes/invocations/invocationsRoute.js +2 -2
- package/src/lambda/routes/invoke-async/invokeAsyncRoute.js +1 -1
- package/src/utils/checkGoVersion.js +1 -1
- package/src/utils/generateHapiPath.js +1 -1
- package/src/utils/splitHandlerPathAndName.js +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,12 @@ To do so, it starts an HTTP server that handles the request's lifecycle like API
|
|
|
38
38
|
|
|
39
39
|
This plugin is updated by its users, I just do maintenance and ensure that PRs are relevant to the community. In other words, if you [find a bug or want a new feature](https://github.com/dherault/serverless-offline/issues), please help us by becoming one of the [contributors](https://github.com/dherault/serverless-offline/graphs/contributors) :v: ! See the [contributing section](#contributing).
|
|
40
40
|
|
|
41
|
+
## Looking for maintainers ⚠️
|
|
42
|
+
|
|
43
|
+
Applications welcome! This package is used by thousands of people daily. Yet we lack a maintainer! If you think you'll be a good fit for the role, please send me an email at dherault/at/gmail.com.
|
|
44
|
+
|
|
45
|
+
See https://github.com/dherault/serverless-offline/issues/1704.
|
|
46
|
+
|
|
41
47
|
## Documentation
|
|
42
48
|
|
|
43
49
|
- [Installation](#installation)
|
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": "
|
|
4
|
+
"version": "13.0.0",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -24,12 +24,8 @@
|
|
|
24
24
|
"prettify:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier --write",
|
|
25
25
|
"test": "mocha --require ./tests/mochaHooks.cjs",
|
|
26
26
|
"test:cov": "NODE_OPTIONS='--experimental-loader @istanbuljs/esm-loader-hook' nyc --reporter=html npm test",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"old-test:log": "npm run build && jest --verbose",
|
|
30
|
-
"old-test:noBuild": "jest --verbose --runInBand --bail",
|
|
31
|
-
"old-test:unit": "jest --verbose --silent --runInBand --config jest.config.units.js",
|
|
32
|
-
"old-test:watch": "SKIP_SETUP=true jest --verbose --watch"
|
|
27
|
+
"test:node": "TEST=unit mocha --require ./tests/mochaHooks.cjs",
|
|
28
|
+
"test:unit": "TEST=unit mocha --require ./tests/mochaHooks.cjs"
|
|
33
29
|
},
|
|
34
30
|
"repository": {
|
|
35
31
|
"type": "git",
|
|
@@ -55,7 +51,7 @@
|
|
|
55
51
|
],
|
|
56
52
|
"author": "David Hérault <dherault@gmail.com> (https://github.com/dherault)",
|
|
57
53
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
54
|
+
"node": ">=18.12.0"
|
|
59
55
|
},
|
|
60
56
|
"standard-version": {
|
|
61
57
|
"skip": {
|
|
@@ -82,47 +78,47 @@
|
|
|
82
78
|
]
|
|
83
79
|
},
|
|
84
80
|
"dependencies": {
|
|
85
|
-
"@aws-sdk/client-lambda": "^3.
|
|
86
|
-
"@hapi/boom": "^10.0.
|
|
87
|
-
"@hapi/h2o2": "^10.0.
|
|
88
|
-
"@hapi/hapi": "^21.
|
|
89
|
-
"@serverless/utils": "^6.
|
|
90
|
-
"array-unflat-js": "^0.1.
|
|
91
|
-
"boxen": "^7.
|
|
92
|
-
"chalk": "^5.
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
81
|
+
"@aws-sdk/client-lambda": "^3.414.0",
|
|
82
|
+
"@hapi/boom": "^10.0.1",
|
|
83
|
+
"@hapi/h2o2": "^10.0.4",
|
|
84
|
+
"@hapi/hapi": "^21.3.2",
|
|
85
|
+
"@serverless/utils": "^6.15.0",
|
|
86
|
+
"array-unflat-js": "^0.1.3",
|
|
87
|
+
"boxen": "^7.1.1",
|
|
88
|
+
"chalk": "^5.3.0",
|
|
89
|
+
"desm": "^1.3.0",
|
|
90
|
+
"execa": "^8.0.1",
|
|
91
|
+
"fs-extra": "^11.1.1",
|
|
92
|
+
"is-wsl": "^3.0.0",
|
|
96
93
|
"java-invoke-local": "0.0.6",
|
|
97
|
-
"jose": "^4.
|
|
94
|
+
"jose": "^4.14.6",
|
|
98
95
|
"js-string-escape": "^1.0.1",
|
|
99
96
|
"jsonpath-plus": "^7.2.0",
|
|
100
97
|
"jsonschema": "^1.4.1",
|
|
101
98
|
"jszip": "^3.10.1",
|
|
102
|
-
"luxon": "^3.
|
|
103
|
-
"node-
|
|
104
|
-
"node-schedule": "^2.1.0",
|
|
105
|
-
"object.hasown": "^1.1.2",
|
|
99
|
+
"luxon": "^3.2.0",
|
|
100
|
+
"node-schedule": "^2.1.1",
|
|
106
101
|
"p-memoize": "^7.1.1",
|
|
107
|
-
"p-retry": "^
|
|
102
|
+
"p-retry": "^6.0.0",
|
|
108
103
|
"velocityjs": "^2.0.6",
|
|
109
|
-
"ws": "^8.
|
|
104
|
+
"ws": "^8.14.2"
|
|
110
105
|
},
|
|
111
106
|
"devDependencies": {
|
|
112
107
|
"@istanbuljs/esm-loader-hook": "^0.2.0",
|
|
113
|
-
"archiver": "^
|
|
114
|
-
"eslint": "^8.
|
|
108
|
+
"archiver": "^6.0.1",
|
|
109
|
+
"eslint": "^8.49.0",
|
|
115
110
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
116
|
-
"eslint-config-prettier": "^
|
|
117
|
-
"eslint-plugin-import": "^2.
|
|
118
|
-
"eslint-plugin-prettier": "^
|
|
111
|
+
"eslint-config-prettier": "^9.0.0",
|
|
112
|
+
"eslint-plugin-import": "^2.28.1",
|
|
113
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
114
|
+
"eslint-plugin-unicorn": "^48.0.1",
|
|
119
115
|
"git-list-updated": "^1.2.1",
|
|
120
|
-
"husky": "^8.0.
|
|
121
|
-
"lint-staged": "^
|
|
116
|
+
"husky": "^8.0.3",
|
|
117
|
+
"lint-staged": "^14.0.1",
|
|
122
118
|
"mocha": "^10.2.0",
|
|
123
119
|
"nyc": "^15.1.0",
|
|
124
|
-
"prettier": "^
|
|
125
|
-
"serverless": "^3.
|
|
120
|
+
"prettier": "^3.0.3",
|
|
121
|
+
"serverless": "^3.35.2",
|
|
126
122
|
"standard-version": "^9.5.0"
|
|
127
123
|
},
|
|
128
124
|
"peerDependencies": {
|
package/src/ServerlessOffline.js
CHANGED
|
@@ -107,8 +107,10 @@ export default class ServerlessOffline {
|
|
|
107
107
|
const eventModules = []
|
|
108
108
|
|
|
109
109
|
if (this.#lambda) {
|
|
110
|
-
eventModules.push(
|
|
111
|
-
|
|
110
|
+
eventModules.push(
|
|
111
|
+
this.#lambda.cleanup(),
|
|
112
|
+
this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT),
|
|
113
|
+
)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
if (this.#alb) {
|
|
@@ -259,13 +261,13 @@ export default class ServerlessOffline {
|
|
|
259
261
|
|
|
260
262
|
// Parse CORS options
|
|
261
263
|
this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
|
|
262
|
-
.
|
|
264
|
+
.replaceAll(' ', '')
|
|
263
265
|
.split(',')
|
|
264
266
|
this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
|
|
265
|
-
.
|
|
267
|
+
.replaceAll(' ', '')
|
|
266
268
|
.split(',')
|
|
267
269
|
this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
|
|
268
|
-
.
|
|
270
|
+
.replaceAll(' ', '')
|
|
269
271
|
.split(',')
|
|
270
272
|
|
|
271
273
|
this.#options.corsConfig = {
|
|
@@ -4,18 +4,16 @@
|
|
|
4
4
|
// .NET CORE
|
|
5
5
|
// export const supportedDotnetcore = new Set([
|
|
6
6
|
// 'dotnet6',
|
|
7
|
-
// 'dotnetcore3.1',
|
|
8
7
|
// ])
|
|
9
8
|
|
|
10
9
|
// GO
|
|
11
10
|
export const supportedGo = new Set(['go1.x'])
|
|
12
11
|
|
|
13
12
|
// JAVA
|
|
14
|
-
export const supportedJava = new Set(['java8', 'java8.al2', 'java11'])
|
|
13
|
+
export const supportedJava = new Set(['java8', 'java8.al2', 'java11', 'java17'])
|
|
15
14
|
|
|
16
15
|
// NODE.JS
|
|
17
16
|
export const supportedNodejs = new Set([
|
|
18
|
-
'nodejs12.x',
|
|
19
17
|
'nodejs14.x',
|
|
20
18
|
'nodejs16.x',
|
|
21
19
|
'nodejs18.x',
|
|
@@ -26,14 +24,15 @@ export const supportedProvided = new Set(['provided', 'provided.al2'])
|
|
|
26
24
|
|
|
27
25
|
// PYTHON
|
|
28
26
|
export const supportedPython = new Set([
|
|
29
|
-
'python3.6',
|
|
30
27
|
'python3.7',
|
|
31
28
|
'python3.8',
|
|
32
29
|
'python3.9',
|
|
30
|
+
'python3.10',
|
|
31
|
+
'python3.11',
|
|
33
32
|
])
|
|
34
33
|
|
|
35
34
|
// RUBY
|
|
36
|
-
export const supportedRuby = new Set(['ruby2.7'])
|
|
35
|
+
export const supportedRuby = new Set(['ruby2.7', 'ruby3.2'])
|
|
37
36
|
|
|
38
37
|
// deprecated runtimes
|
|
39
38
|
// https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
|
package/src/events/alb/Alb.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { log } from '@serverless/utils/log.js'
|
|
2
1
|
import AlbEventDefinition from './AlbEventDefinition.js'
|
|
3
2
|
import HttpServer from './HttpServer.js'
|
|
4
3
|
|
|
@@ -15,11 +14,6 @@ export default class Alb {
|
|
|
15
14
|
this.#lambda = lambda
|
|
16
15
|
this.#options = options
|
|
17
16
|
this.#serverless = serverless
|
|
18
|
-
|
|
19
|
-
log.warning(`
|
|
20
|
-
Application Load Balancer (ALB) support in serverless-offline is experimental.
|
|
21
|
-
Please file an issue for any bugs, missing features or other feedback: https://github.com/dherault/serverless-offline/issues
|
|
22
|
-
`)
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
start() {
|
|
@@ -105,13 +105,9 @@ export default class HttpServer {
|
|
|
105
105
|
if (request.method === 'options') {
|
|
106
106
|
response.statusCode = 200
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} else {
|
|
112
|
-
response.headers['access-control-expose-headers'] =
|
|
113
|
-
'content-type, content-length, etag'
|
|
114
|
-
}
|
|
108
|
+
response.headers['access-control-expose-headers'] =
|
|
109
|
+
request.headers['access-control-expose-headers'] ||
|
|
110
|
+
'content-type, content-length, etag'
|
|
115
111
|
response.headers['access-control-max-age'] = 60 * 10
|
|
116
112
|
|
|
117
113
|
if (request.headers['access-control-request-headers']) {
|
|
@@ -61,7 +61,7 @@ export default function authMatchPolicyResource(policyResource, resource) {
|
|
|
61
61
|
// for the requested resource and the resource defined in the policy
|
|
62
62
|
// Need to create a regex replacing ? with one character and * with any number of characters
|
|
63
63
|
const regExp = new RegExp(
|
|
64
|
-
parsedPolicyResource.path.
|
|
64
|
+
parsedPolicyResource.path.replaceAll('*', '.*').replaceAll('?', '.'),
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
return regExp.test(parsedResource.path)
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
2
|
import { log } from '@serverless/utils/log.js'
|
|
3
|
+
import { join } from 'desm'
|
|
5
4
|
import OfflineEndpoint from './OfflineEndpoint.js'
|
|
6
5
|
|
|
7
6
|
const { entries } = Object
|
|
8
7
|
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
-
|
|
11
8
|
// velocity template defaults
|
|
12
9
|
const defaultRequestTemplate = readFileSync(
|
|
13
|
-
|
|
10
|
+
join(import.meta.url, 'templates/offline-default.req.vm'),
|
|
14
11
|
'utf8',
|
|
15
12
|
)
|
|
16
13
|
const defaultResponseTemplate = readFileSync(
|
|
17
|
-
|
|
14
|
+
join(import.meta.url, 'templates/offline-default.res.vm'),
|
|
18
15
|
'utf8',
|
|
19
16
|
)
|
|
20
17
|
|
|
21
18
|
function getResponseContentType(fep) {
|
|
22
19
|
if (fep.response && fep.response.headers['Content-Type']) {
|
|
23
|
-
return fep.response.headers['Content-Type'].
|
|
20
|
+
return fep.response.headers['Content-Type'].replaceAll(/'/gm, '')
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
return 'application/json'
|
|
@@ -57,8 +57,8 @@ export default class HttpServer {
|
|
|
57
57
|
|
|
58
58
|
async #loadCerts(httpsProtocol) {
|
|
59
59
|
const [cert, key] = await Promise.all([
|
|
60
|
-
readFile(resolve(httpsProtocol, 'cert.pem'), '
|
|
61
|
-
readFile(resolve(httpsProtocol, 'key.pem'), '
|
|
60
|
+
readFile(resolve(httpsProtocol, 'cert.pem'), 'utf8'),
|
|
61
|
+
readFile(resolve(httpsProtocol, 'key.pem'), 'utf8'),
|
|
62
62
|
])
|
|
63
63
|
|
|
64
64
|
return {
|
|
@@ -164,13 +164,9 @@ export default class HttpServer {
|
|
|
164
164
|
if (request.method === 'options') {
|
|
165
165
|
response.statusCode = 200
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} else {
|
|
171
|
-
response.headers['access-control-expose-headers'] =
|
|
172
|
-
'content-type, content-length, etag'
|
|
173
|
-
}
|
|
167
|
+
response.headers['access-control-expose-headers'] =
|
|
168
|
+
request.headers['access-control-expose-headers'] ||
|
|
169
|
+
'content-type, content-length, etag'
|
|
174
170
|
response.headers['access-control-max-age'] = 60 * 10
|
|
175
171
|
|
|
176
172
|
if (request.headers['access-control-request-headers']) {
|
|
@@ -323,14 +319,12 @@ export default class HttpServer {
|
|
|
323
319
|
(endpoint.isHttpApi &&
|
|
324
320
|
serverlessAuthorizerOptions?.enableSimpleResponses) ||
|
|
325
321
|
false,
|
|
326
|
-
identitySource:
|
|
327
|
-
serverlessAuthorizerOptions?.identitySource ||
|
|
328
|
-
'method.request.header.Authorization',
|
|
322
|
+
identitySource: serverlessAuthorizerOptions?.identitySource,
|
|
329
323
|
identityValidationExpression:
|
|
330
324
|
serverlessAuthorizerOptions?.identityValidationExpression || '(.*)',
|
|
331
|
-
payloadVersion:
|
|
332
|
-
? '
|
|
333
|
-
:
|
|
325
|
+
payloadVersion: endpoint.isHttpApi
|
|
326
|
+
? serverlessAuthorizerOptions?.payloadVersion || '2.0'
|
|
327
|
+
: '1.0',
|
|
334
328
|
resultTtlInSeconds:
|
|
335
329
|
serverlessAuthorizerOptions?.resultTtlInSeconds || '300',
|
|
336
330
|
}
|
|
@@ -351,6 +345,16 @@ export default class HttpServer {
|
|
|
351
345
|
assign(authorizerOptions, endpoint.authorizer)
|
|
352
346
|
}
|
|
353
347
|
|
|
348
|
+
if (
|
|
349
|
+
!authorizerOptions.identitySource &&
|
|
350
|
+
!(
|
|
351
|
+
authorizerOptions.type === 'request' &&
|
|
352
|
+
authorizerOptions.resultTtlInSeconds === 0
|
|
353
|
+
)
|
|
354
|
+
) {
|
|
355
|
+
authorizerOptions.identitySource = 'method.request.header.Authorization'
|
|
356
|
+
}
|
|
357
|
+
|
|
354
358
|
// Create a unique scheme per endpoint
|
|
355
359
|
// This allows the methodArn on the event property to be set appropriately
|
|
356
360
|
const authKey = `${functionKey}-${authFunctionName}-${method}-${path}`
|
|
@@ -502,9 +506,9 @@ export default class HttpServer {
|
|
|
502
506
|
: ''
|
|
503
507
|
|
|
504
508
|
const schemas =
|
|
505
|
-
endpoint?.request?.schemas
|
|
506
|
-
?
|
|
507
|
-
:
|
|
509
|
+
endpoint?.request?.schemas === undefined
|
|
510
|
+
? ''
|
|
511
|
+
: endpoint.request.schemas[contentType]
|
|
508
512
|
|
|
509
513
|
// https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
|
|
510
514
|
// so we have to do it ourselves
|
|
@@ -519,7 +523,7 @@ export default class HttpServer {
|
|
|
519
523
|
request.payload.length > 1
|
|
520
524
|
) {
|
|
521
525
|
try {
|
|
522
|
-
if (!request.payload || request.payload.length
|
|
526
|
+
if (!request.payload || request.payload.length === 0) {
|
|
523
527
|
request.payload = '{}'
|
|
524
528
|
}
|
|
525
529
|
|
|
@@ -640,7 +644,7 @@ export default class HttpServer {
|
|
|
640
644
|
for (const [key, value] of entries(endpoint.responses)) {
|
|
641
645
|
if (
|
|
642
646
|
key !== 'default' &&
|
|
643
|
-
|
|
647
|
+
`^${value.selectionPattern || key}$`.test(errorMessage)
|
|
644
648
|
) {
|
|
645
649
|
responseName = key
|
|
646
650
|
break
|
|
@@ -685,11 +689,8 @@ export default class HttpServer {
|
|
|
685
689
|
headerValue = valueArray[3]
|
|
686
690
|
? jsonPath(result, valueArray.slice(3).join('.'))
|
|
687
691
|
: result
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
} else {
|
|
691
|
-
headerValue = headerValue.toString()
|
|
692
|
-
}
|
|
692
|
+
|
|
693
|
+
headerValue = headerValue == null ? '' : String(headerValue)
|
|
693
694
|
} else {
|
|
694
695
|
log.notice()
|
|
695
696
|
|
|
@@ -702,7 +703,7 @@ export default class HttpServer {
|
|
|
702
703
|
log.notice()
|
|
703
704
|
}
|
|
704
705
|
} else {
|
|
705
|
-
headerValue =
|
|
706
|
+
headerValue = /^'.*'$/.test(value) ? value.slice(1, -1) : value // See #34
|
|
706
707
|
}
|
|
707
708
|
// Applies the header;
|
|
708
709
|
if (headerValue === '') {
|
|
@@ -748,7 +749,7 @@ export default class HttpServer {
|
|
|
748
749
|
const { responseTemplates } = chosenResponse
|
|
749
750
|
|
|
750
751
|
if (typeof responseTemplates === 'object') {
|
|
751
|
-
if (keys(responseTemplates).length) {
|
|
752
|
+
if (keys(responseTemplates).length > 0) {
|
|
752
753
|
// BAD IMPLEMENTATION: first key in responseTemplates
|
|
753
754
|
const responseTemplate = responseTemplates[responseContentType]
|
|
754
755
|
|
|
@@ -1137,7 +1138,7 @@ export default class HttpServer {
|
|
|
1137
1138
|
|
|
1138
1139
|
const resourceRoutes = parseResources(this.#serverless.service.resources)
|
|
1139
1140
|
|
|
1140
|
-
if (!resourceRoutes ||
|
|
1141
|
+
if (!resourceRoutes || keys(resourceRoutes).length === 0) {
|
|
1141
1142
|
return
|
|
1142
1143
|
}
|
|
1143
1144
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const IDENTITY_SOURCE_TYPE_HEADER = 'header'
|
|
15
15
|
const IDENTITY_SOURCE_TYPE_QUERYSTRING = 'querystring'
|
|
16
|
+
const IDENTITY_SOURCE_TYPE_NONE = 'none'
|
|
16
17
|
|
|
17
18
|
export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
18
19
|
const authFunName = authorizerOptions.name
|
|
@@ -65,38 +66,50 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
65
66
|
const methodArn = `arn:aws:execute-api:${provider.region}:${accountId}:${apiId}/${provider.stage}/${httpMethod}${resourcePath}`
|
|
66
67
|
|
|
67
68
|
let authorization
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
switch (identitySourceType) {
|
|
70
|
+
case IDENTITY_SOURCE_TYPE_HEADER: {
|
|
71
|
+
const headers = request.raw.req.headers ?? {}
|
|
72
|
+
authorization = headers[identitySourceField]
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
case IDENTITY_SOURCE_TYPE_QUERYSTRING: {
|
|
76
|
+
const queryStringParameters = parseQueryStringParameters(url) ?? {}
|
|
77
|
+
authorization = queryStringParameters[identitySourceField]
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
case IDENTITY_SOURCE_TYPE_NONE: {
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`No Authorization source has been specified. This should never happen. (λ: ${authFunName})`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
let finalAuthorization
|
|
91
|
+
if (identitySourceType !== IDENTITY_SOURCE_TYPE_NONE) {
|
|
92
|
+
if (authorization === undefined) {
|
|
93
|
+
log.error(
|
|
94
|
+
`Identity Source is null for ${identitySourceType} ${identitySourceField} (λ: ${authFunName})`,
|
|
95
|
+
)
|
|
96
|
+
return Boom.unauthorized(
|
|
97
|
+
'User is not authorized to access this resource',
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const identityValidationExpression = new RegExp(
|
|
102
|
+
authorizerOptions.identityValidationExpression,
|
|
83
103
|
)
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
const matchedAuthorization =
|
|
105
|
+
identityValidationExpression.test(authorization)
|
|
106
|
+
finalAuthorization = matchedAuthorization ? authorization : ''
|
|
107
|
+
|
|
108
|
+
log.debug(
|
|
109
|
+
`Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`,
|
|
86
110
|
)
|
|
87
111
|
}
|
|
88
112
|
|
|
89
|
-
const identityValidationExpression = new RegExp(
|
|
90
|
-
authorizerOptions.identityValidationExpression,
|
|
91
|
-
)
|
|
92
|
-
const matchedAuthorization =
|
|
93
|
-
identityValidationExpression.test(authorization)
|
|
94
|
-
const finalAuthorization = matchedAuthorization ? authorization : ''
|
|
95
|
-
|
|
96
|
-
log.debug(
|
|
97
|
-
`Retrieved ${identitySourceField} ${identitySourceType} "${finalAuthorization}"`,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
113
|
if (authorizerOptions.payloadVersion === '1.0') {
|
|
101
114
|
event = {
|
|
102
115
|
...event,
|
|
@@ -148,17 +161,10 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
148
161
|
|
|
149
162
|
// methodArn is the ARN of the function we are running we are authorizing access to (or not)
|
|
150
163
|
// Account ID and API ID are not simulated
|
|
151
|
-
|
|
152
|
-
event
|
|
153
|
-
...event,
|
|
154
|
-
type: 'REQUEST',
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
164
|
+
event = {
|
|
165
|
+
...event,
|
|
157
166
|
// This is safe since type: 'TOKEN' cannot have payload format 2.0
|
|
158
|
-
|
|
159
|
-
...event,
|
|
160
|
-
type: 'TOKEN',
|
|
161
|
-
}
|
|
167
|
+
type: authorizerOptions.type === 'request' ? 'REQUEST' : 'TOKEN',
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
const lambdaFunction = lambda.get(authFunName)
|
|
@@ -296,5 +302,10 @@ export default function createAuthScheme(authorizerOptions, provider, lambda) {
|
|
|
296
302
|
)
|
|
297
303
|
}
|
|
298
304
|
|
|
305
|
+
if (authorizerOptions.resultTtlInSeconds === 0) {
|
|
306
|
+
identitySourceType = IDENTITY_SOURCE_TYPE_NONE
|
|
307
|
+
return finalizeAuthScheme()
|
|
308
|
+
}
|
|
309
|
+
|
|
299
310
|
return finalizeAuthScheme()
|
|
300
311
|
}
|
|
@@ -67,7 +67,7 @@ export default function createAuthScheme(jwtOptions) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
let scopes = null
|
|
70
|
-
if (jwtOptions.scopes && jwtOptions.scopes.length) {
|
|
70
|
+
if (jwtOptions.scopes && jwtOptions.scopes.length > 0) {
|
|
71
71
|
if (!scope) {
|
|
72
72
|
log.notice(`JWT Token missing valid scope`)
|
|
73
73
|
|
|
@@ -20,10 +20,6 @@ function javaMatches(value) {
|
|
|
20
20
|
return this.match(new RegExp(value, 'm'))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function javaReplaceAll(oldValue, newValue) {
|
|
24
|
-
return this.replace(new RegExp(oldValue, 'gm'), newValue)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
function javaReplaceFirst(oldValue, newValue) {
|
|
28
24
|
return this.replace(new RegExp(oldValue, 'm'), newValue)
|
|
29
25
|
}
|
|
@@ -74,7 +70,6 @@ const {
|
|
|
74
70
|
equalsIgnoreCase,
|
|
75
71
|
matches,
|
|
76
72
|
regionMatches,
|
|
77
|
-
replaceAll,
|
|
78
73
|
replaceFirst,
|
|
79
74
|
},
|
|
80
75
|
} = String
|
|
@@ -85,7 +80,6 @@ export default function runInPollutedScope(runScope) {
|
|
|
85
80
|
prototype.equalsIgnoreCase = javaEqualsIgnoreCase
|
|
86
81
|
prototype.matches = javaMatches
|
|
87
82
|
prototype.regionMatches = javaRegionMatches
|
|
88
|
-
prototype.replaceAll = javaReplaceAll
|
|
89
83
|
prototype.replaceFirst = javaReplaceFirst
|
|
90
84
|
|
|
91
85
|
const result = runScope()
|
|
@@ -95,7 +89,6 @@ export default function runInPollutedScope(runScope) {
|
|
|
95
89
|
prototype.equalsIgnoreCase = equalsIgnoreCase
|
|
96
90
|
prototype.matches = matches
|
|
97
91
|
prototype.regionMatches = regionMatches
|
|
98
|
-
prototype.replaceAll = replaceAll
|
|
99
92
|
prototype.replaceFirst = replaceFirst
|
|
100
93
|
|
|
101
94
|
return result
|
|
@@ -14,7 +14,7 @@ const { assign, entries, fromEntries } = Object
|
|
|
14
14
|
|
|
15
15
|
function escapeJavaScript(x) {
|
|
16
16
|
if (typeof x === 'string') {
|
|
17
|
-
return jsEscapeString(x).
|
|
17
|
+
return jsEscapeString(x).replaceAll('\\n', '\n') // See #26,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (isPlainObject(x)) {
|
|
@@ -136,7 +136,7 @@ export default class VelocityContext {
|
|
|
136
136
|
Buffer.from(x.toString(), 'binary').toString('base64'),
|
|
137
137
|
escapeJavaScript,
|
|
138
138
|
parseJson: parse,
|
|
139
|
-
urlDecode: (x) => decodeURIComponent(x.
|
|
139
|
+
urlDecode: (x) => decodeURIComponent(x.replaceAll('+', ' ')),
|
|
140
140
|
urlEncode: encodeURI,
|
|
141
141
|
},
|
|
142
142
|
}
|
|
@@ -33,20 +33,25 @@ function renderVelocityString(velocityString, context) {
|
|
|
33
33
|
|
|
34
34
|
// Haaaa Velocity... this language sure loves strings a lot
|
|
35
35
|
switch (renderResult) {
|
|
36
|
-
case 'undefined':
|
|
37
|
-
return undefined
|
|
36
|
+
case 'undefined': {
|
|
37
|
+
return undefined
|
|
38
|
+
} // But we don't, we want JavaScript types
|
|
38
39
|
|
|
39
|
-
case 'null':
|
|
40
|
+
case 'null': {
|
|
40
41
|
return null
|
|
42
|
+
}
|
|
41
43
|
|
|
42
|
-
case 'true':
|
|
44
|
+
case 'true': {
|
|
43
45
|
return true
|
|
46
|
+
}
|
|
44
47
|
|
|
45
|
-
case 'false':
|
|
48
|
+
case 'false': {
|
|
46
49
|
return false
|
|
50
|
+
}
|
|
47
51
|
|
|
48
|
-
default:
|
|
52
|
+
default: {
|
|
49
53
|
return tryToParseJSON(renderResult)
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -78,21 +78,25 @@ export default class Schedule {
|
|
|
78
78
|
|
|
79
79
|
switch (unit) {
|
|
80
80
|
case 'minute':
|
|
81
|
-
case 'minutes':
|
|
81
|
+
case 'minutes': {
|
|
82
82
|
return `*/${number} * * * *`
|
|
83
|
+
}
|
|
83
84
|
|
|
84
85
|
case 'hour':
|
|
85
|
-
case 'hours':
|
|
86
|
+
case 'hours': {
|
|
86
87
|
return `0 */${number} * * *`
|
|
88
|
+
}
|
|
87
89
|
|
|
88
90
|
case 'day':
|
|
89
|
-
case 'days':
|
|
91
|
+
case 'days': {
|
|
90
92
|
return `0 0 */${number} * *`
|
|
93
|
+
}
|
|
91
94
|
|
|
92
|
-
default:
|
|
95
|
+
default: {
|
|
93
96
|
log.error(`scheduler: Invalid rate syntax '${rate}', will not schedule`)
|
|
94
97
|
|
|
95
98
|
return null
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -16,7 +16,7 @@ export default class ScheduleEvent {
|
|
|
16
16
|
source = 'aws.events'
|
|
17
17
|
|
|
18
18
|
// format of aws displaying the time, e.g.: 2020-02-09T14:13:57Z
|
|
19
|
-
time = new Date().toISOString().
|
|
19
|
+
time = new Date().toISOString().replaceAll(/\.(.*)(?=Z)/g, '')
|
|
20
20
|
|
|
21
21
|
version = '0'
|
|
22
22
|
|
|
@@ -19,8 +19,8 @@ export default class HttpServer {
|
|
|
19
19
|
|
|
20
20
|
async #loadCerts(httpsProtocol) {
|
|
21
21
|
const [cert, key] = await Promise.all([
|
|
22
|
-
readFile(resolve(httpsProtocol, 'cert.pem'), '
|
|
23
|
-
readFile(resolve(httpsProtocol, 'key.pem'), '
|
|
22
|
+
readFile(resolve(httpsProtocol, 'cert.pem'), 'utf8'),
|
|
23
|
+
readFile(resolve(httpsProtocol, 'key.pem'), 'utf8'),
|
|
24
24
|
])
|
|
25
25
|
|
|
26
26
|
return {
|
package/src/index.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
// TODO remove with node.js v16.9+ support
|
|
2
|
-
import 'object.hasown/auto'
|
|
3
|
-
|
|
4
|
-
// install global fetch
|
|
5
|
-
// TODO remove `node-fetch` module and use global built-in with node.js v18+ support
|
|
6
|
-
if (globalThis.fetch === undefined) {
|
|
7
|
-
const { default: fetch, Headers } = await import('node-fetch')
|
|
8
|
-
globalThis.fetch = fetch
|
|
9
|
-
globalThis.Headers = Headers
|
|
10
|
-
}
|
|
11
|
-
|
|
12
1
|
export { default } from './ServerlessOffline.js'
|
|
@@ -250,7 +250,7 @@ export default class LambdaFunction {
|
|
|
250
250
|
entries(zip.files).map(async ([filename, jsZipObj]) => {
|
|
251
251
|
const fileData = await jsZipObj.async('nodebuffer')
|
|
252
252
|
if (filename.endsWith('/')) {
|
|
253
|
-
return
|
|
253
|
+
return undefined
|
|
254
254
|
}
|
|
255
255
|
await ensureDir(join(this.#codeDir, dirname(filename)))
|
|
256
256
|
return writeFile(join(this.#codeDir, filename), fileData, {
|
|
@@ -95,13 +95,7 @@ export default class DockerContainer {
|
|
|
95
95
|
if (this.#layers.length > 0) {
|
|
96
96
|
log.verbose(`Found layers, checking provider type`)
|
|
97
97
|
|
|
98
|
-
if (this.#provider.name.toLowerCase()
|
|
99
|
-
log.warning(
|
|
100
|
-
`Provider ${
|
|
101
|
-
this.#provider.name
|
|
102
|
-
} is Unsupported. Layers are only supported on aws.`,
|
|
103
|
-
)
|
|
104
|
-
} else {
|
|
98
|
+
if (this.#provider.name.toLowerCase() === 'aws') {
|
|
105
99
|
let layerDir = this.#dockerOptions.layersDir
|
|
106
100
|
|
|
107
101
|
if (!layerDir) {
|
|
@@ -142,6 +136,12 @@ export default class DockerContainer {
|
|
|
142
136
|
)
|
|
143
137
|
}
|
|
144
138
|
dockerArgs.push('-v', `${layerDir}:/opt:ro,delegated`)
|
|
139
|
+
} else {
|
|
140
|
+
log.warning(
|
|
141
|
+
`Provider ${
|
|
142
|
+
this.#provider.name
|
|
143
|
+
} is Unsupported. Layers are only supported on aws.`,
|
|
144
|
+
)
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -313,7 +313,7 @@ export default class DockerContainer {
|
|
|
313
313
|
entries(zip.files).map(async ([filename, jsZipObj]) => {
|
|
314
314
|
const fileData = await jsZipObj.async('nodebuffer')
|
|
315
315
|
if (filename.endsWith(sep)) {
|
|
316
|
-
return
|
|
316
|
+
return undefined
|
|
317
317
|
}
|
|
318
318
|
await ensureDir(join(layerDir, dirname(filename)))
|
|
319
319
|
return writeFile(join(layerDir, filename), fileData, {
|
|
@@ -106,7 +106,7 @@ export default class GoRunner {
|
|
|
106
106
|
// Get go env to run this locally
|
|
107
107
|
if (!this.#goEnv) {
|
|
108
108
|
const goEnvResponse = await execa('go', ['env'], {
|
|
109
|
-
encoding: '
|
|
109
|
+
encoding: 'utf8',
|
|
110
110
|
stdio: 'pipe',
|
|
111
111
|
})
|
|
112
112
|
|
|
@@ -136,7 +136,7 @@ export default class GoRunner {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const { stdout, stderr } = await execa(`./tmp`, {
|
|
139
|
-
encoding: '
|
|
139
|
+
encoding: 'utf8',
|
|
140
140
|
env: {
|
|
141
141
|
...this.#env,
|
|
142
142
|
...this.#goEnv,
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { EOL, platform } from 'node:os'
|
|
3
|
-
import { delimiter,
|
|
3
|
+
import { delimiter, join as pathJoin, relative } from 'node:path'
|
|
4
4
|
import process, { cwd, nextTick } from 'node:process'
|
|
5
5
|
import { createInterface } from 'node:readline'
|
|
6
|
-
import { fileURLToPath } from 'node:url'
|
|
7
6
|
import { log } from '@serverless/utils/log.js'
|
|
7
|
+
import { join } from 'desm'
|
|
8
8
|
import { splitHandlerPathAndName } from '../../../utils/index.js'
|
|
9
9
|
|
|
10
10
|
const { parse, stringify } = JSON
|
|
11
11
|
const { assign, hasOwn } = Object
|
|
12
12
|
|
|
13
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
-
|
|
15
13
|
export default class PythonRunner {
|
|
16
14
|
static #payloadIdentifier = '__offline_payload__'
|
|
17
15
|
|
|
@@ -32,7 +30,7 @@ export default class PythonRunner {
|
|
|
32
30
|
const runtimeDir = platform() === 'win32' ? 'Scripts' : 'bin'
|
|
33
31
|
|
|
34
32
|
process.env.PATH = [
|
|
35
|
-
|
|
33
|
+
pathJoin(process.env.VIRTUAL_ENV, runtimeDir),
|
|
36
34
|
delimiter,
|
|
37
35
|
process.env.PATH,
|
|
38
36
|
].join('')
|
|
@@ -44,7 +42,7 @@ export default class PythonRunner {
|
|
|
44
42
|
pythonExecutable,
|
|
45
43
|
[
|
|
46
44
|
'-u',
|
|
47
|
-
|
|
45
|
+
join(import.meta.url, 'invoke.py'),
|
|
48
46
|
relative(cwd(), handlerPath),
|
|
49
47
|
handlerName,
|
|
50
48
|
],
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { EOL, platform } from 'node:os'
|
|
2
|
-
import {
|
|
2
|
+
import { relative } from 'node:path'
|
|
3
3
|
import { cwd } from 'node:process'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
4
|
import { log } from '@serverless/utils/log.js'
|
|
5
|
+
import { join } from 'desm'
|
|
6
6
|
import { execa } from 'execa'
|
|
7
7
|
import { splitHandlerPathAndName } from '../../../utils/index.js'
|
|
8
8
|
|
|
9
9
|
const { parse, stringify } = JSON
|
|
10
10
|
const { hasOwn } = Object
|
|
11
11
|
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
|
-
|
|
14
12
|
export default class RubyRunner {
|
|
15
13
|
static #payloadIdentifier = '__offline_payload__'
|
|
16
14
|
|
|
@@ -86,7 +84,7 @@ export default class RubyRunner {
|
|
|
86
84
|
const { stderr, stdout } = await execa(
|
|
87
85
|
runtime,
|
|
88
86
|
[
|
|
89
|
-
|
|
87
|
+
join(import.meta.url, 'invoke.rb'),
|
|
90
88
|
relative(cwd(), this.#handlerPath),
|
|
91
89
|
this.#handlerName,
|
|
92
90
|
],
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { dirname, resolve } from 'node:path'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
1
|
import { MessageChannel, Worker } from 'node:worker_threads'
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
|
-
const workerThreadHelperPath = resolve(__dirname, 'workerThreadHelper.js')
|
|
2
|
+
import { join } from 'desm'
|
|
7
3
|
|
|
8
4
|
export default class WorkerThreadRunner {
|
|
9
5
|
#workerThread = null
|
|
@@ -11,17 +7,20 @@ export default class WorkerThreadRunner {
|
|
|
11
7
|
constructor(funOptions, env) {
|
|
12
8
|
const { codeDir, functionKey, handler, servicePath, timeout } = funOptions
|
|
13
9
|
|
|
14
|
-
this.#workerThread = new Worker(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
this.#workerThread = new Worker(
|
|
11
|
+
join(import.meta.url, 'workerThreadHelper.js'),
|
|
12
|
+
{
|
|
13
|
+
// don't pass process.env from the main process!
|
|
14
|
+
env,
|
|
15
|
+
workerData: {
|
|
16
|
+
codeDir,
|
|
17
|
+
functionKey,
|
|
18
|
+
handler,
|
|
19
|
+
servicePath,
|
|
20
|
+
timeout,
|
|
21
|
+
},
|
|
23
22
|
},
|
|
24
|
-
|
|
23
|
+
)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
// () => Promise<number>
|
|
@@ -12,7 +12,7 @@ export default class InvocationsController {
|
|
|
12
12
|
const functionNames = this.#lambda.listFunctionNames()
|
|
13
13
|
if (functionNames.length === 0 || !functionNames.includes(functionName)) {
|
|
14
14
|
log.error(
|
|
15
|
-
`Attempt to invoke function '${functionName}' failed. Function does not
|
|
15
|
+
`Attempt to invoke function '${functionName}' failed. Function does not exist.`,
|
|
16
16
|
)
|
|
17
17
|
// Conforms to the actual response from AWS Lambda when invoking a non-existent
|
|
18
18
|
// function. Details on the error are provided in the Payload.Message key
|
|
@@ -25,11 +25,11 @@ export default function invocationsRoute(lambda, options) {
|
|
|
25
25
|
// check client context header was set
|
|
26
26
|
if (clientContextHeader) {
|
|
27
27
|
const clientContextBuffer = Buffer.from(clientContextHeader, 'base64')
|
|
28
|
-
clientContext = parse(clientContextBuffer.toString('
|
|
28
|
+
clientContext = parse(clientContextBuffer.toString('utf8'))
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// check if payload was set, if not, default event is an empty object
|
|
32
|
-
const event = payload.length > 0 ? parse(payload.toString('
|
|
32
|
+
const event = payload.length > 0 ? parse(payload.toString('utf8')) : {}
|
|
33
33
|
|
|
34
34
|
const invokeResults = await invocationsController.invoke(
|
|
35
35
|
functionName,
|
|
@@ -8,7 +8,7 @@ export default function splitHandlerPathAndName(handler) {
|
|
|
8
8
|
// filename: source
|
|
9
9
|
// path: ./src/somefolder/source
|
|
10
10
|
// name: LambdaFunctions::Handler.process
|
|
11
|
-
if (handler
|
|
11
|
+
if (/::/.test(handler)) {
|
|
12
12
|
const prepathDelimiter = handler.lastIndexOf('/')
|
|
13
13
|
const prepath = handler.substr(0, prepathDelimiter + 1) // include '/' for path
|
|
14
14
|
const postpath = handler.substr(prepathDelimiter + 1)
|