local-serverless-stack 0.17.2 → 1.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/CHANGELOG.md +327 -0
- package/README.md +1150 -221
- package/assets/icon.svg +37 -0
- package/assets/logo.svg +38 -0
- package/bin/cli.js +384 -217
- package/bin/i18n.js +498 -0
- package/dist/client/config-discovery.d.ts.map +1 -1
- package/dist/client/config-discovery.js +2 -1
- package/dist/client/config-discovery.js.map +1 -1
- package/dist/client/http.d.ts +7 -0
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +3 -2
- package/dist/client/http.js.map +1 -1
- package/dist/client/lifecycle.d.ts +1 -4
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +2 -8
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/namespaces/health.d.ts +1 -1
- package/dist/client/namespaces/health.d.ts.map +1 -1
- package/dist/client/namespaces/queues.d.ts.map +1 -1
- package/dist/client/namespaces/queues.js +9 -0
- package/dist/client/namespaces/queues.js.map +1 -1
- package/dist/client/namespaces/seeds.d.ts +1 -1
- package/dist/client/namespaces/services.d.ts +34 -0
- package/dist/client/namespaces/services.d.ts.map +1 -1
- package/dist/client/namespaces/services.js +3 -0
- package/dist/client/namespaces/services.js.map +1 -1
- package/dist/client/types.d.ts +17 -5
- package/dist/client/types.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +15 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +86 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/protocol.d.ts +36 -0
- package/dist/mcp/protocol.d.ts.map +1 -0
- package/dist/mcp/protocol.js +96 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +41 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +18 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +323 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/server/dev/dynamo-proxy.d.ts.map +1 -1
- package/dist/server/dev/dynamo-proxy.js +16 -2
- package/dist/server/dev/dynamo-proxy.js.map +1 -1
- package/dist/server/engine/backends/self-backend.d.ts +11 -2
- package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
- package/dist/server/engine/backends/self-backend.js +42 -1
- package/dist/server/engine/backends/self-backend.js.map +1 -1
- package/dist/server/engine/dispatch/dispatcher.js +2 -2
- package/dist/server/engine/dispatch/dispatcher.js.map +1 -1
- package/dist/server/engine/engine-manager.d.ts +6 -1
- package/dist/server/engine/engine-manager.d.ts.map +1 -1
- package/dist/server/engine/engine-manager.js +30 -27
- package/dist/server/engine/engine-manager.js.map +1 -1
- package/dist/server/engine/http/is-aws-request.d.ts +8 -0
- package/dist/server/engine/http/is-aws-request.d.ts.map +1 -0
- package/dist/server/engine/http/is-aws-request.js +53 -0
- package/dist/server/engine/http/is-aws-request.js.map +1 -0
- package/dist/server/engine/http/router.d.ts.map +1 -1
- package/dist/server/engine/http/router.js +7 -1
- package/dist/server/engine/http/router.js.map +1 -1
- package/dist/server/engine/store/engine-store.d.ts +1 -0
- package/dist/server/engine/store/engine-store.d.ts.map +1 -1
- package/dist/server/engine/store/engine-store.js +2 -1
- package/dist/server/engine/store/engine-store.js.map +1 -1
- package/dist/server/engine/store/memory-store.d.ts +16 -0
- package/dist/server/engine/store/memory-store.d.ts.map +1 -0
- package/dist/server/engine/store/memory-store.js +167 -0
- package/dist/server/engine/store/memory-store.js.map +1 -0
- package/dist/server/engine/store/wal.d.ts +3 -0
- package/dist/server/engine/store/wal.d.ts.map +1 -1
- package/dist/server/engine/store/wal.js +35 -1
- package/dist/server/engine/store/wal.js.map +1 -1
- package/dist/server/index.js +159 -44
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/config.d.ts.map +1 -1
- package/dist/server/routes/config.js +25 -52
- package/dist/server/routes/config.js.map +1 -1
- package/dist/server/routes/lambdas.d.ts.map +1 -1
- package/dist/server/routes/lambdas.js +65 -0
- package/dist/server/routes/lambdas.js.map +1 -1
- package/dist/server/routes/services.d.ts.map +1 -1
- package/dist/server/routes/services.js +275 -24
- package/dist/server/routes/services.js.map +1 -1
- package/dist/server/services/bind-host.d.ts +30 -0
- package/dist/server/services/bind-host.d.ts.map +1 -0
- package/dist/server/services/bind-host.js +182 -0
- package/dist/server/services/bind-host.js.map +1 -0
- package/dist/server/services/cache-manager.d.ts +2 -1
- package/dist/server/services/cache-manager.d.ts.map +1 -1
- package/dist/server/services/cache-manager.js +4 -14
- package/dist/server/services/cache-manager.js.map +1 -1
- package/dist/server/services/config-manager.d.ts +28 -35
- package/dist/server/services/config-manager.d.ts.map +1 -1
- package/dist/server/services/config-manager.js +475 -189
- package/dist/server/services/config-manager.js.map +1 -1
- package/dist/server/services/dynamo-explorer.d.ts +1 -0
- package/dist/server/services/dynamo-explorer.d.ts.map +1 -1
- package/dist/server/services/dynamo-explorer.js +28 -6
- package/dist/server/services/dynamo-explorer.js.map +1 -1
- package/dist/server/services/explorer-region.d.ts +2 -0
- package/dist/server/services/explorer-region.d.ts.map +1 -0
- package/dist/server/services/explorer-region.js +23 -0
- package/dist/server/services/explorer-region.js.map +1 -0
- package/dist/server/services/gateway-manager.d.ts.map +1 -1
- package/dist/server/services/gateway-manager.js +18 -5
- package/dist/server/services/gateway-manager.js.map +1 -1
- package/dist/server/services/invocation-activity.d.ts +45 -0
- package/dist/server/services/invocation-activity.d.ts.map +1 -0
- package/dist/server/services/invocation-activity.js +102 -0
- package/dist/server/services/invocation-activity.js.map +1 -0
- package/dist/server/services/lambda-runtime-manager.d.ts +7 -1
- package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
- package/dist/server/services/lambda-runtime-manager.js +118 -10
- package/dist/server/services/lambda-runtime-manager.js.map +1 -1
- package/dist/server/services/opensearch-explorer.d.ts.map +1 -1
- package/dist/server/services/opensearch-explorer.js +14 -10
- package/dist/server/services/opensearch-explorer.js.map +1 -1
- package/dist/server/services/project-scope.d.ts +2 -0
- package/dist/server/services/project-scope.d.ts.map +1 -0
- package/dist/server/services/project-scope.js +23 -0
- package/dist/server/services/project-scope.js.map +1 -0
- package/dist/server/services/queue-inspector.d.ts.map +1 -1
- package/dist/server/services/queue-inspector.js +15 -6
- package/dist/server/services/queue-inspector.js.map +1 -1
- package/dist/server/services/resource-provisioner.d.ts +0 -2
- package/dist/server/services/resource-provisioner.d.ts.map +1 -1
- package/dist/server/services/resource-provisioner.js +24 -14
- package/dist/server/services/resource-provisioner.js.map +1 -1
- package/dist/server/services/s3-explorer.js +3 -3
- package/dist/server/services/s3-explorer.js.map +1 -1
- package/dist/server/services/secrets-explorer.d.ts.map +1 -1
- package/dist/server/services/secrets-explorer.js +10 -3
- package/dist/server/services/secrets-explorer.js.map +1 -1
- package/dist/server/services/seed-manager.d.ts.map +1 -1
- package/dist/server/services/seed-manager.js +17 -8
- package/dist/server/services/seed-manager.js.map +1 -1
- package/dist/server/services/serverless-state-parser.d.ts +10 -0
- package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
- package/dist/server/services/serverless-state-parser.js +20 -1
- package/dist/server/services/serverless-state-parser.js.map +1 -1
- package/dist/server/services/service-registrar.d.ts.map +1 -1
- package/dist/server/services/service-registrar.js +19 -5
- package/dist/server/services/service-registrar.js.map +1 -1
- package/dist/server/services/service-scanner.d.ts +20 -0
- package/dist/server/services/service-scanner.d.ts.map +1 -0
- package/dist/server/services/service-scanner.js +173 -0
- package/dist/server/services/service-scanner.js.map +1 -0
- package/dist/server/services/source-watcher.d.ts.map +1 -1
- package/dist/server/services/source-watcher.js +31 -6
- package/dist/server/services/source-watcher.js.map +1 -1
- package/dist/ui/assets/ApisPage-BOuimwWz.js +1 -0
- package/dist/ui/assets/BucketDetailPage-vLYwhoQt.js +1 -0
- package/dist/ui/assets/BucketsPage-Cu5lU1Hp.js +1 -0
- package/dist/ui/assets/DynamoPage-y3TTEPUL.js +1 -0
- package/dist/ui/assets/DynamoTablePage-BnmSRZjc.js +3 -0
- package/dist/ui/assets/LambdaDetailPage-B5q_d27t.js +4 -0
- package/dist/ui/assets/LambdasPage-CJpRoEDx.js +1 -0
- package/dist/ui/assets/OnboardingPage-D02dymQi.js +1 -0
- package/dist/ui/assets/OpenSearchCollectionPage-B_wWTlfL.js +1 -0
- package/dist/ui/assets/OpenSearchPage-BLEgfF_g.js +1 -0
- package/dist/ui/assets/{OverviewPage-DS6XJ7kG.css → OverviewPage-C1pBJ8aP.css} +1 -1
- package/dist/ui/assets/OverviewPage-EdJWiAt6.js +1 -0
- package/dist/ui/assets/QueueDetailPage-ByEYHvHe.js +1 -0
- package/dist/ui/assets/QueuesPage-CvwYHSll.js +1 -0
- package/dist/ui/assets/SecretsPage-DASw8Ntf.js +1 -0
- package/dist/ui/assets/ServiceDetailPage-BMsUDg6L.js +2 -0
- package/dist/ui/assets/ServicesPage-2JuiIiRr.js +2 -0
- package/dist/ui/assets/SettingsPage-DStO4bgN.js +1 -0
- package/dist/ui/assets/engine-D0mFFNbL.js +1 -0
- package/dist/ui/assets/index-BIEznsi4.js +37 -0
- package/dist/ui/assets/index-BRMvNP3Y.css +1 -0
- package/dist/ui/assets/onboarding-DQmlvyWU.js +1 -0
- package/dist/ui/assets/resourceIcons-baecADXA.js +1 -0
- package/dist/ui/assets/triggerIcons-DIPWQkJ3.js +1 -0
- package/dist/ui/favicon.svg +37 -0
- package/dist/ui/index.html +5 -3
- package/package.json +11 -11
- package/dist/server/engine/aoss-sidecar.d.ts +0 -13
- package/dist/server/engine/aoss-sidecar.d.ts.map +0 -1
- package/dist/server/engine/aoss-sidecar.js +0 -93
- package/dist/server/engine/aoss-sidecar.js.map +0 -1
- package/dist/server/engine/backends/localstack-backend.d.ts +0 -25
- package/dist/server/engine/backends/localstack-backend.d.ts.map +0 -1
- package/dist/server/engine/backends/localstack-backend.js +0 -199
- package/dist/server/engine/backends/localstack-backend.js.map +0 -1
- package/dist/server/engine/engine-backend.d.ts +0 -17
- package/dist/server/engine/engine-backend.d.ts.map +0 -1
- package/dist/server/engine/engine-backend.js +0 -7
- package/dist/server/engine/engine-backend.js.map +0 -1
- package/dist/server/services/localstack-manager.d.ts +0 -17
- package/dist/server/services/localstack-manager.d.ts.map +0 -1
- package/dist/server/services/localstack-manager.js +0 -31
- package/dist/server/services/localstack-manager.js.map +0 -1
- package/dist/ui/assets/ApisPage-CXYcWDhm.js +0 -1
- package/dist/ui/assets/BucketDetailPage-BwS2--Pe.js +0 -1
- package/dist/ui/assets/BucketsPage-khuiYc9C.js +0 -1
- package/dist/ui/assets/DynamoPage-aYo0QC0N.js +0 -1
- package/dist/ui/assets/DynamoTablePage-rFuV2pgd.js +0 -3
- package/dist/ui/assets/LambdaDetailPage-CJmu8M_N.js +0 -4
- package/dist/ui/assets/LambdasPage-DniNyKMe.js +0 -1
- package/dist/ui/assets/OpenSearchCollectionPage-BBwjgbB9.js +0 -1
- package/dist/ui/assets/OpenSearchPage-D5RxJGRo.js +0 -1
- package/dist/ui/assets/OverviewPage-Cyt1Xdp8.js +0 -1
- package/dist/ui/assets/QueueDetailPage-3FPsowIp.js +0 -1
- package/dist/ui/assets/QueuesPage-CUoDDgHr.js +0 -1
- package/dist/ui/assets/SecretsPage-B_mTnUf3.js +0 -1
- package/dist/ui/assets/ServiceDetailPage-BcdSjA6e.js +0 -2
- package/dist/ui/assets/ServicesPage-CU6fb3f8.js +0 -2
- package/dist/ui/assets/SettingsPage-BG8cYxgC.js +0 -1
- package/dist/ui/assets/index-BxwA-UT0.css +0 -1
- package/dist/ui/assets/index-DGsXIPJ-.js +0 -37
- package/packages/serverless-plugin/README.md +0 -115
- package/packages/serverless-plugin/dist/index.d.ts +0 -2
- package/packages/serverless-plugin/dist/index.d.ts.map +0 -1
- package/packages/serverless-plugin/dist/index.js +0 -107
- package/packages/serverless-plugin/dist/index.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { projectCacheSegment } from './project-scope.js';
|
|
4
5
|
// Symlink-stable spelling of a directory (e.g. macOS /tmp -> /private/tmp),
|
|
5
6
|
// so the project identity matches the plugin's symlink-resolved process.cwd().
|
|
6
7
|
function realpathOrSelf(dir) {
|
|
@@ -11,37 +12,219 @@ function realpathOrSelf(dir) {
|
|
|
11
12
|
return dir;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
15
|
+
// One port for the whole stack: the REST API, the dashboard and the AWS wire
|
|
16
|
+
// all answer here by default. Deliberately outside 4566–4599, which a real
|
|
17
|
+
// LocalStack install intercepts on some hosts (Docker Desktop/WSL2).
|
|
18
|
+
//
|
|
19
|
+
// Setting `serverPort` and `selfEngine.port` to DIFFERENT values splits them
|
|
20
|
+
// back onto two listeners — see isSingleListener().
|
|
21
|
+
const DEFAULT_PORT = 14566;
|
|
22
|
+
// `secrets` is deliberately absent (and rejected on write): it holds seed
|
|
23
|
+
// material that belongs in the config file or the environment, never in a
|
|
24
|
+
// payload that crosses the dashboard API.
|
|
25
|
+
const BLOCKED_CONFIG_KEYS = new Set(['secrets']);
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// What a package command written through PUT /api/config may become
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
//
|
|
30
|
+
// A package command is handed to serverless-packager.ts, which tokenizes it and
|
|
31
|
+
// calls spawn(firstToken, restTokens) with `packageArgs` appended to the argv.
|
|
32
|
+
// PUT /api/config is unauthenticated, so an unconstrained command — or an
|
|
33
|
+
// unconstrained arg list — is arbitrary code execution on this host:
|
|
34
|
+
// `PUT /api/config {"packageCommand":"/bin/sh","packageArgs":["-c","…"]}`
|
|
35
|
+
// followed by `POST /api/services/package` runs whatever the caller wrote. That
|
|
36
|
+
// is the same class the /start and /install runner allowlists already close;
|
|
37
|
+
// this is the third door into it.
|
|
38
|
+
//
|
|
39
|
+
// Fencing only the command's FIRST token does NOT close it, and that version of
|
|
40
|
+
// this check was walked straight through: every package manager on the list is
|
|
41
|
+
// itself an interpreter, one subcommand in.
|
|
42
|
+
//
|
|
43
|
+
// npm exec -c '<shell string>' spawn('npm', ['exec','-c',…]) runs the string
|
|
44
|
+
// npx -c '<shell string>' as a shell command — no shell:true required,
|
|
45
|
+
// the payload is read straight off the argv
|
|
46
|
+
// npx -y <pkg> · npm exec -p <pkg> · yarn dlx · pnpm dlx fetch and run any package
|
|
47
|
+
// yarn node -e '<js>' · yarn exec · pnpm exec run an interpreter
|
|
48
|
+
// npm i <pkg> · yarn add <pkg> run a fetched package's install scripts
|
|
49
|
+
//
|
|
50
|
+
// Each of those satisfies "the first token names a package manager". So the
|
|
51
|
+
// fence is a grammar over the WHOLE command instead of a check on one token —
|
|
52
|
+
// the shapes below and nothing else:
|
|
53
|
+
//
|
|
54
|
+
// npm | yarn | pnpm run | run-script [script] [args…]
|
|
55
|
+
// serverless | sls | osls package [args…]
|
|
56
|
+
// npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
57
|
+
//
|
|
58
|
+
// plus BLOCKED_PACKAGE_FLAGS applied to every token of the command AND to every
|
|
59
|
+
// `packageArgs` element, since those reach the same argv without passing
|
|
60
|
+
// through the tokenizer at all.
|
|
61
|
+
//
|
|
62
|
+
// RESIDUAL, stated plainly rather than papered over. `npm run <script>` still
|
|
63
|
+
// runs whatever the service's own package.json declares, and `serverless
|
|
64
|
+
// package` still reads the service's own serverless.yml (plugins included).
|
|
65
|
+
// Both are the project's own code, which the operator already trusts by
|
|
66
|
+
// pointing LSS at the directory; and forbidding them would mean the dashboard
|
|
67
|
+
// could not set a package command at all, which is what the onboarding flow is
|
|
68
|
+
// built on. What is gone is the part that made the /start and /install
|
|
69
|
+
// allowlists worthless: through this API a caller can no longer choose the
|
|
70
|
+
// program, only ask the project's own build to run.
|
|
71
|
+
//
|
|
72
|
+
// The fence is on the API and deliberately NOT on the read path — a
|
|
73
|
+
// hand-edited lss.config.json and LSS_PACKAGE_COMMAND are the operator's own
|
|
74
|
+
// shell, and anyone who can write either already runs code on this host.
|
|
75
|
+
/**
|
|
76
|
+
* Runner names accepted as a command's first token, in the order the error
|
|
77
|
+
* message lists them. No entry contains a path separator, so membership alone
|
|
78
|
+
* rejects `/bin/sh`, `/usr/bin/env`, `./tool` and `..\x.exe`.
|
|
79
|
+
*/
|
|
80
|
+
const ALLOWED_PACKAGE_RUNNERS = [
|
|
81
|
+
'npm', 'npx', 'yarn', 'pnpm', 'serverless', 'sls', 'osls',
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* The subcommands each runner may be followed by — the wall the first-token
|
|
85
|
+
* check was missing. `run`/`run-script` execute a script the project declared;
|
|
86
|
+
* every other verb these CLIs expose either takes a command off the argv
|
|
87
|
+
* (`exec`, `x`, `node`, `dlx`) or fetches a package and runs its code (`add`,
|
|
88
|
+
* `install`, `create`, `plugin install`). The Serverless CLIs get `package` for
|
|
89
|
+
* the same reason: `serverless plugin install <pkg>` is a fetch-and-run, and
|
|
90
|
+
* packaging is the only thing this setting exists to do.
|
|
91
|
+
*
|
|
92
|
+
* A bare `yarn package` (yarn 1's implicit `run`) is rejected on purpose: the
|
|
93
|
+
* implicit form is exactly what makes `yarn node -e '<js>'` and `yarn dlx <pkg>`
|
|
94
|
+
* indistinguishable from a script name, so the explicit `yarn run package` is
|
|
95
|
+
* required instead. `npx` is absent because it is a launcher rather than a
|
|
96
|
+
* program — validatePackageCommand() strips it and validates what it launches.
|
|
97
|
+
*/
|
|
98
|
+
const PACKAGE_RUNNER_VERBS = {
|
|
99
|
+
npm: ['run', 'run-script'],
|
|
100
|
+
yarn: ['run'],
|
|
101
|
+
pnpm: ['run'],
|
|
102
|
+
serverless: ['package'],
|
|
103
|
+
sls: ['package'],
|
|
104
|
+
osls: ['package'],
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The packages `npx` may fetch and run. Restricted to the Serverless CLIs
|
|
108
|
+
* because that is the entire reason npx appears here — LSS's own default is
|
|
109
|
+
* `npx serverless package`, i.e. "run the Serverless CLI without a global
|
|
110
|
+
* install". Anything else npx can reach is a package of the caller's choosing.
|
|
111
|
+
*/
|
|
112
|
+
const NPX_PACKAGES = ['serverless', 'sls', 'osls'];
|
|
113
|
+
/**
|
|
114
|
+
* The only flags tolerated between `npx` and the package name. `-y` suppresses
|
|
115
|
+
* the "install it?" prompt and does nothing else; every other npx flag either
|
|
116
|
+
* names a different package to run (`-p`/`--package`) or takes a command string
|
|
117
|
+
* (`-c`/`--call`), which is the bypass itself.
|
|
118
|
+
*/
|
|
119
|
+
const NPX_PREFIX_FLAGS = ['-y', '--yes'];
|
|
120
|
+
/**
|
|
121
|
+
* Flags that make an otherwise-allowed command run something other than the
|
|
122
|
+
* project's own build, whichever runner is in front of them. The grammar above
|
|
123
|
+
* pins the program; these pin what that program is allowed to be pointed at:
|
|
124
|
+
*
|
|
125
|
+
* --node-options npm/yarn/pnpm/npx set NODE_OPTIONS on the child, which
|
|
126
|
+
* is `--require <file>` under another name (the same hole
|
|
127
|
+
* BLOCKED_PACKAGE_ENV_KEYS closes on the env side)
|
|
128
|
+
* --script-shell npm chooses the binary that interprets the script;
|
|
129
|
+
* --shell/--shell-mode are the yarn/pnpm spellings
|
|
130
|
+
* --call npm exec's inline command string
|
|
131
|
+
* --userconfig/--globalconfig/--use-yarnrc
|
|
132
|
+
* point the manager at another rc file, which can itself
|
|
133
|
+
* set script-shell, node-options or yarn 1's `yarn-path`
|
|
134
|
+
* --registry `npx serverless package` fetches `serverless` when the
|
|
135
|
+
* service has no local copy, and npm's arg parser reads
|
|
136
|
+
* its own config flags from anywhere in the argv — so a
|
|
137
|
+
* registry named after the package name still decides
|
|
138
|
+
* which code that fetch runs
|
|
139
|
+
* --prefix/--cwd/--dir run the scripts of a package.json somewhere else
|
|
140
|
+
* --eval/--print/--require/--import/--loader/--experimental-loader/--input-type
|
|
141
|
+
* node's own code-loading flags, refused in case any
|
|
142
|
+
* runner forwards its argv to node
|
|
143
|
+
*
|
|
144
|
+
* Deliberately absent: `--package` (npm exec's package selector) and every
|
|
145
|
+
* short flag. Both are already unreachable — `npm exec` is not an allowed verb
|
|
146
|
+
* and npx accepts no flag but `-y` — while `-c`, `-p` and `-r` are `--config`,
|
|
147
|
+
* `--package` and `--region` to the Serverless CLI, so blocking them would
|
|
148
|
+
* break `sls package -c custom.yml` for nothing.
|
|
149
|
+
*
|
|
150
|
+
* Compared with dashes and underscores stripped, so `--nodeOptions` and
|
|
151
|
+
* `--node_options` cannot spell their way past the set.
|
|
152
|
+
*/
|
|
153
|
+
const BLOCKED_PACKAGE_FLAGS = new Set([
|
|
154
|
+
'nodeoptions',
|
|
155
|
+
'scriptshell',
|
|
156
|
+
'shell',
|
|
157
|
+
'shellmode',
|
|
158
|
+
'call',
|
|
159
|
+
'userconfig',
|
|
160
|
+
'globalconfig',
|
|
161
|
+
'useyarnrc',
|
|
162
|
+
'registry',
|
|
163
|
+
'prefix',
|
|
164
|
+
'cwd',
|
|
165
|
+
'dir',
|
|
166
|
+
'eval',
|
|
167
|
+
'print',
|
|
168
|
+
'require',
|
|
169
|
+
'import',
|
|
170
|
+
'loader',
|
|
171
|
+
'experimentalloader',
|
|
172
|
+
'inputtype',
|
|
173
|
+
]);
|
|
174
|
+
// Env vars that make the spawned child load attacker-chosen code no matter
|
|
175
|
+
// WHICH binary runs, so they walk straight around the runner allowlist above:
|
|
176
|
+
// NODE_OPTIONS `--require /tmp/x.js` into every node process
|
|
177
|
+
// NODE_REPL_EXTERNAL_MODULE loads a module into node ahead of the program
|
|
178
|
+
// LD_PRELOAD / LD_AUDIT inject a shared object into the dynamic linker
|
|
179
|
+
// LD_LIBRARY_PATH re-points the linker at attacker libraries
|
|
180
|
+
// DYLD_INSERT_LIBRARIES / DYLD_LIBRARY_PATH the macOS spellings of both
|
|
181
|
+
// PATH decides WHICH `npm` runs at all — see below
|
|
182
|
+
// NODE_PATH prepends a module search root for every require
|
|
183
|
+
// Stored lowercase and compared case-insensitively: env lookup is
|
|
184
|
+
// case-insensitive on Windows, so `node_options` would otherwise be the same
|
|
185
|
+
// bypass one keystroke away.
|
|
186
|
+
//
|
|
187
|
+
// PATH is the one that looks harmless and is not. `runServerlessPackage` spawns
|
|
188
|
+
// WITHOUT a shell, but a shell was never what resolved the binary: libuv runs
|
|
189
|
+
// execvp(3) after installing the child's environment, so the child's PATH — not
|
|
190
|
+
// the orchestrator's — decides which file named `npm` executes. Verified by
|
|
191
|
+
// spawning `npm` with a prepended directory: the planted binary ran. That makes
|
|
192
|
+
// PATH equivalent to naming an arbitrary command, which is exactly what
|
|
193
|
+
// ALLOWED_PACKAGE_RUNNERS exists to prevent. The packaging child inherits the
|
|
194
|
+
// orchestrator's PATH and no caller may replace it.
|
|
195
|
+
const BLOCKED_PACKAGE_ENV_KEYS = new Set([
|
|
196
|
+
'node_options',
|
|
197
|
+
'node_repl_external_module',
|
|
198
|
+
'ld_preload',
|
|
199
|
+
'ld_audit',
|
|
200
|
+
'ld_library_path',
|
|
201
|
+
'dyld_insert_libraries',
|
|
202
|
+
'dyld_library_path',
|
|
203
|
+
'path',
|
|
204
|
+
'node_path',
|
|
205
|
+
]);
|
|
19
206
|
const EDITABLE_CONFIG_KEYS = {
|
|
20
207
|
serverPort: { kind: 'port' },
|
|
21
|
-
localstackPort: { kind: 'port' },
|
|
22
|
-
localstackEndpoint: { kind: 'string' },
|
|
23
|
-
mode: { kind: 'string', enum: ['managed', 'external'] },
|
|
24
|
-
localstackEdition: { kind: 'string', enum: ['community', 'pro'] },
|
|
25
|
-
localstackVersion: { kind: 'string' },
|
|
26
|
-
localstackImage: { kind: 'string' },
|
|
27
208
|
enableDynamoProxy: { kind: 'boolean' },
|
|
28
209
|
dynamoProxyPort: { kind: 'port' },
|
|
29
210
|
region: { kind: 'string' },
|
|
30
|
-
services: { kind: 'stringArray' },
|
|
31
211
|
persistence: { kind: 'boolean' },
|
|
32
212
|
debug: { kind: 'boolean' },
|
|
33
213
|
seedsDir: { kind: 'string' },
|
|
34
214
|
stateDir: { kind: 'string' },
|
|
35
215
|
autoPackage: { kind: 'boolean' },
|
|
36
|
-
packageCommand: { kind: '
|
|
37
|
-
packageArgs: { kind: '
|
|
38
|
-
packageEnv: { kind: '
|
|
216
|
+
packageCommand: { kind: 'packageCommand' },
|
|
217
|
+
packageArgs: { kind: 'packageArgs' },
|
|
218
|
+
packageEnv: { kind: 'envRecord' },
|
|
39
219
|
servicePackaging: {
|
|
40
220
|
kind: 'object',
|
|
41
221
|
entrySubKeys: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
222
|
+
// Same fences as the global keys above — a per-service override is spawned
|
|
223
|
+
// by the exact same code path, so validating only the global copy would
|
|
224
|
+
// leave the door open one nesting level down.
|
|
225
|
+
packageCommand: { kind: 'packageCommand' },
|
|
226
|
+
packageArgs: { kind: 'packageArgs' },
|
|
227
|
+
packageEnv: { kind: 'envRecord' },
|
|
45
228
|
packageTimeoutMs: { kind: 'positiveInt' },
|
|
46
229
|
},
|
|
47
230
|
},
|
|
@@ -54,6 +237,9 @@ const EDITABLE_CONFIG_KEYS = {
|
|
|
54
237
|
watch: { kind: 'boolean' },
|
|
55
238
|
invokePortOffset: { kind: 'positiveInt' },
|
|
56
239
|
invokeHost: { kind: 'string' },
|
|
240
|
+
lazy: { kind: 'boolean' },
|
|
241
|
+
idleTimeoutMs: { kind: 'nonNegativeInt' },
|
|
242
|
+
maxWarmWorkers: { kind: 'nonNegativeInt' },
|
|
57
243
|
},
|
|
58
244
|
},
|
|
59
245
|
serviceRuntime: {
|
|
@@ -66,7 +252,6 @@ const EDITABLE_CONFIG_KEYS = {
|
|
|
66
252
|
watch: { kind: 'boolean' },
|
|
67
253
|
},
|
|
68
254
|
},
|
|
69
|
-
engine: { kind: 'string', enum: ['localstack', 'self'] },
|
|
70
255
|
selfEngine: {
|
|
71
256
|
kind: 'object',
|
|
72
257
|
subKeys: {
|
|
@@ -79,13 +264,6 @@ const EDITABLE_CONFIG_KEYS = {
|
|
|
79
264
|
fallbackEndpoint: { kind: 'string' },
|
|
80
265
|
},
|
|
81
266
|
},
|
|
82
|
-
aossSidecar: {
|
|
83
|
-
kind: 'object',
|
|
84
|
-
subKeys: {
|
|
85
|
-
enabled: { kind: 'boolean' },
|
|
86
|
-
port: { kind: 'port' },
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
267
|
branding: {
|
|
90
268
|
kind: 'object',
|
|
91
269
|
subKeys: {
|
|
@@ -111,13 +289,6 @@ const EDITABLE_CONFIG_KEYS = {
|
|
|
111
289
|
// is unset.
|
|
112
290
|
const CONFIG_ENV_OVERRIDES = {
|
|
113
291
|
serverPort: ['LSS_DASHBOARD_PORT', 'PORT'],
|
|
114
|
-
localstackPort: ['LSS_LOCALSTACK_PORT'],
|
|
115
|
-
localstackEndpoint: ['LSS_LOCALSTACK_ENDPOINT'],
|
|
116
|
-
mode: ['LSS_LOCALSTACK_MODE'],
|
|
117
|
-
localstackEdition: ['LSS_LOCALSTACK_EDITION'],
|
|
118
|
-
localstackVersion: ['LSS_LOCALSTACK_VERSION'],
|
|
119
|
-
localstackImage: ['LSS_LOCALSTACK_IMAGE'],
|
|
120
|
-
localstackAuthToken: ['LOCALSTACK_AUTH_TOKEN'],
|
|
121
292
|
// Note: the deprecated unprefixed ENABLE_DYNAMO_PROXY is NOT listed — it is
|
|
122
293
|
// only a getter-time fallback (isEnableDynamoProxy) that a file value always
|
|
123
294
|
// beats, so it never masks a saved value.
|
|
@@ -132,12 +303,95 @@ const CONFIG_ENV_OVERRIDES = {
|
|
|
132
303
|
packageCommand: ['LSS_PACKAGE_COMMAND'],
|
|
133
304
|
packageTimeoutMs: ['LSS_PACKAGE_TIMEOUT_MS'],
|
|
134
305
|
lambdaRuntime: ['LSS_LAMBDA_RUNTIME', 'LSS_LAMBDA_EXECUTION', 'LSS_LAMBDA_WATCH', 'LSS_INVOKE_HOST'],
|
|
135
|
-
|
|
136
|
-
selfEngine: ['LSS_ENGINE_PORT'],
|
|
306
|
+
selfEngine: ['LSS_ENGINE_PORT', 'LSS_ENGINE_DATA_DIR'],
|
|
137
307
|
};
|
|
138
308
|
function isPlainObject(value) {
|
|
139
309
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
140
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* The argv a package command would actually spawn, tokenized EXACTLY the way
|
|
313
|
+
* parseCommand() in serverless-packager.ts does (same regex, same
|
|
314
|
+
* per-quoted-segment stripping). Mirroring it is the whole point — the tokens
|
|
315
|
+
* validated here must be the tokens handed to spawn(), or the check and the
|
|
316
|
+
* execution disagree and the grammar means nothing. `"/bin/sh"` resolves to
|
|
317
|
+
* `/bin/sh` (rejected) and `'npm'` to `npm` (accepted), in both places.
|
|
318
|
+
* Keep the two in step if the packager's tokenizer ever changes.
|
|
319
|
+
*/
|
|
320
|
+
function packageCommandTokens(raw) {
|
|
321
|
+
const tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
322
|
+
return tokens.map(token => token.replace(/"([^"]*)"|'([^']*)'/g, '$1$2'));
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Comparable spelling of a flag token: the name before any `=value`, lowercased
|
|
326
|
+
* with dashes and underscores removed (`--Node-Options=--require x` →
|
|
327
|
+
* `nodeoptions`). Only the name is compared — the value is what the flag would
|
|
328
|
+
* point at, and every value is dangerous once the name is.
|
|
329
|
+
*/
|
|
330
|
+
function normalizeFlagName(token) {
|
|
331
|
+
return token.split('=')[0].toLowerCase().replace(/[-_]/g, '');
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A token that redirects the command at a program, shell or rc file of the
|
|
335
|
+
* caller's choosing. Only tokens that look like flags are screened, so a script
|
|
336
|
+
* or file name that happens to collide (`npm run print`) is untouched.
|
|
337
|
+
*/
|
|
338
|
+
function isBlockedPackageFlag(token) {
|
|
339
|
+
return token.startsWith('-') && BLOCKED_PACKAGE_FLAGS.has(normalizeFlagName(token));
|
|
340
|
+
}
|
|
341
|
+
function blockedFlagError(label, token) {
|
|
342
|
+
return `"${label}" cannot contain "${token}" — that flag points the package command at a program, shell or config file of the caller's choosing`;
|
|
343
|
+
}
|
|
344
|
+
/** `"run" or "run-script"` — verbs quoted the way the error message reads best. */
|
|
345
|
+
function quotedList(values) {
|
|
346
|
+
const quoted = values.map(value => `"${value}"`);
|
|
347
|
+
return quoted.length === 1 ? quoted[0] : `${quoted.slice(0, -1).join(', ')} or ${quoted[quoted.length - 1]}`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Validate a `packageCommand` against the grammar documented above:
|
|
351
|
+
* runner → subcommand → free arguments, with `npx` stripped down to the
|
|
352
|
+
* Serverless CLI it launches, and no blocked flag anywhere in the argv.
|
|
353
|
+
*/
|
|
354
|
+
function validatePackageCommand(key, raw) {
|
|
355
|
+
const tokens = packageCommandTokens(raw);
|
|
356
|
+
// Two cursors because `npx serverless package` has two layers: `rest` is the
|
|
357
|
+
// slice the grammar is currently reading (it loses the `npx …` prefix), while
|
|
358
|
+
// `runner` names the program that will finally execute and therefore whose
|
|
359
|
+
// subcommand list applies.
|
|
360
|
+
let rest = tokens;
|
|
361
|
+
let runner = rest[0] ?? '';
|
|
362
|
+
if (runner === 'npx') {
|
|
363
|
+
rest = rest.slice(1);
|
|
364
|
+
while (NPX_PREFIX_FLAGS.includes(rest[0])) {
|
|
365
|
+
rest = rest.slice(1);
|
|
366
|
+
}
|
|
367
|
+
const spec = rest[0] ?? '';
|
|
368
|
+
if (spec.startsWith('-')) {
|
|
369
|
+
return `"${key}": npx may carry only ${quotedList(NPX_PREFIX_FLAGS)} before the package name — "${spec}" can name another package to run, or a command string to run as a shell`;
|
|
370
|
+
}
|
|
371
|
+
// `serverless@3.38.0` is the same program with a version pin. The `@` at
|
|
372
|
+
// index 0 belongs to a scope (`@scope/pkg`), which is never in the list.
|
|
373
|
+
const at = spec.lastIndexOf('@');
|
|
374
|
+
runner = at > 0 ? spec.slice(0, at) : spec;
|
|
375
|
+
if (!NPX_PACKAGES.includes(runner)) {
|
|
376
|
+
return `"${key}": npx may only run the Serverless CLI (${NPX_PACKAGES.join(', ')}) — "${spec}" is a package of the caller's choosing`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else if (!ALLOWED_PACKAGE_RUNNERS.includes(runner)) {
|
|
380
|
+
return `"${key}" must start with one of: ${ALLOWED_PACKAGE_RUNNERS.join(', ')} — "${runner}" is not an allowed package runner`;
|
|
381
|
+
}
|
|
382
|
+
const verbs = PACKAGE_RUNNER_VERBS[runner];
|
|
383
|
+
const verb = rest[1] ?? '';
|
|
384
|
+
if (!verbs.includes(verb)) {
|
|
385
|
+
return `"${key}": "${runner}" may only be followed by ${quotedList(verbs)} — `
|
|
386
|
+
+ (verb
|
|
387
|
+
? `"${verb}" can run a program the caller chose instead of the project's build`
|
|
388
|
+
: 'the command names no subcommand at all');
|
|
389
|
+
}
|
|
390
|
+
// Screens the whole argv, not just the tail: a blocked flag is blocked
|
|
391
|
+
// wherever it sits, and the runner/verb tokens never start with a dash.
|
|
392
|
+
const blocked = tokens.find(isBlockedPackageFlag);
|
|
393
|
+
return blocked === undefined ? null : blockedFlagError(key, blocked);
|
|
394
|
+
}
|
|
141
395
|
function validateValue(key, value, spec) {
|
|
142
396
|
switch (spec.kind) {
|
|
143
397
|
case 'port':
|
|
@@ -148,6 +402,11 @@ function validateValue(key, value, spec) {
|
|
|
148
402
|
return Number.isInteger(value) && value > 0
|
|
149
403
|
? null
|
|
150
404
|
: `"${key}" must be a positive integer`;
|
|
405
|
+
// 0 is meaningful for these (idleTimeoutMs: 0 = never unload).
|
|
406
|
+
case 'nonNegativeInt':
|
|
407
|
+
return Number.isInteger(value) && value >= 0
|
|
408
|
+
? null
|
|
409
|
+
: `"${key}" must be an integer >= 0`;
|
|
151
410
|
case 'boolean':
|
|
152
411
|
return typeof value === 'boolean' ? null : `"${key}" must be a boolean`;
|
|
153
412
|
case 'string':
|
|
@@ -157,14 +416,46 @@ function validateValue(key, value, spec) {
|
|
|
157
416
|
return spec.enum && !spec.enum.includes(value)
|
|
158
417
|
? `"${key}" must be one of: ${spec.enum.join(', ')}`
|
|
159
418
|
: null;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
419
|
+
// A command that is spawned, not just stored: on top of the non-empty string
|
|
420
|
+
// check it must match the package-command grammar. See PACKAGE_RUNNER_VERBS
|
|
421
|
+
// for why an unfenced string — or a first-token-only check — is remote code
|
|
422
|
+
// execution here, and for the residual the grammar does not close.
|
|
423
|
+
case 'packageCommand': {
|
|
424
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
425
|
+
return `"${key}" must be a non-empty string`;
|
|
426
|
+
}
|
|
427
|
+
return validatePackageCommand(key, value);
|
|
428
|
+
}
|
|
429
|
+
// Appended to the spawned argv verbatim, with no tokenizer in between, so
|
|
430
|
+
// `packageCommand:"npm"` + `packageArgs:["exec","-c","<shell>"]` would be
|
|
431
|
+
// the identical bypass one key across. The grammar above already pins the
|
|
432
|
+
// program and its subcommand; the flag screen is what stops an arg from
|
|
433
|
+
// re-pointing that program somewhere else.
|
|
434
|
+
case 'packageArgs': {
|
|
435
|
+
if (!Array.isArray(value) || !value.every(v => typeof v === 'string')) {
|
|
436
|
+
return `"${key}" must be an array of strings`;
|
|
437
|
+
}
|
|
438
|
+
const index = value.findIndex(isBlockedPackageFlag);
|
|
439
|
+
return index === -1 ? null : blockedFlagError(`${key}[${index}]`, value[index]);
|
|
440
|
+
}
|
|
164
441
|
case 'stringRecord':
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
442
|
+
case 'envRecord': {
|
|
443
|
+
if (!isPlainObject(value) || !Object.values(value).every(v => typeof v === 'string')) {
|
|
444
|
+
return `"${key}" must be an object of string values`;
|
|
445
|
+
}
|
|
446
|
+
// An env map is merged into the packaging child's environment, where a
|
|
447
|
+
// handful of names are loader/interpreter hooks: they run code in the
|
|
448
|
+
// child whatever program the command grammar let through, which would
|
|
449
|
+
// make that grammar decorative.
|
|
450
|
+
if (spec.kind === 'envRecord') {
|
|
451
|
+
for (const envKey of Object.keys(value)) {
|
|
452
|
+
if (BLOCKED_PACKAGE_ENV_KEYS.has(envKey.toLowerCase())) {
|
|
453
|
+
return `"${key}.${envKey}" cannot be set — it injects code into the packaging process`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
168
459
|
case 'object': {
|
|
169
460
|
if (!isPlainObject(value)) {
|
|
170
461
|
return `"${key}" must be an object`;
|
|
@@ -172,7 +463,12 @@ function validateValue(key, value, spec) {
|
|
|
172
463
|
// null subvalues are always allowed — they delete the subkey on merge.
|
|
173
464
|
if (spec.subKeys) {
|
|
174
465
|
for (const [subKey, subValue] of Object.entries(value)) {
|
|
175
|
-
|
|
466
|
+
// hasOwnProperty, not a plain read: `constructor`/`toString` would
|
|
467
|
+
// otherwise resolve to an Object.prototype member, pass this guard
|
|
468
|
+
// and reach the file as unvalidated JSON.
|
|
469
|
+
const subSpec = Object.prototype.hasOwnProperty.call(spec.subKeys, subKey)
|
|
470
|
+
? spec.subKeys[subKey]
|
|
471
|
+
: undefined;
|
|
176
472
|
if (!subSpec)
|
|
177
473
|
return `unknown config key "${key}.${subKey}"`;
|
|
178
474
|
if (subValue === null)
|
|
@@ -189,7 +485,9 @@ function validateValue(key, value, spec) {
|
|
|
189
485
|
if (!isPlainObject(entryValue))
|
|
190
486
|
return `"${key}.${entryKey}" must be an object`;
|
|
191
487
|
for (const [subKey, subValue] of Object.entries(entryValue)) {
|
|
192
|
-
const subSpec = spec.entrySubKeys
|
|
488
|
+
const subSpec = Object.prototype.hasOwnProperty.call(spec.entrySubKeys, subKey)
|
|
489
|
+
? spec.entrySubKeys[subKey]
|
|
490
|
+
: undefined;
|
|
193
491
|
if (!subSpec)
|
|
194
492
|
return `unknown config key "${key}.${entryKey}.${subKey}"`;
|
|
195
493
|
if (subValue === null)
|
|
@@ -249,15 +547,16 @@ export class ConfigManager {
|
|
|
249
547
|
this.config = JSON.parse(content);
|
|
250
548
|
this.configPath = candidate;
|
|
251
549
|
console.log(`✅ Configuration loaded from ${candidate}`);
|
|
252
|
-
break;
|
|
253
550
|
}
|
|
254
551
|
catch (error) {
|
|
255
552
|
console.warn(`⚠️ Failed to parse config file ${candidate}:`, error instanceof Error ? error.message : 'Unknown error');
|
|
553
|
+
continue;
|
|
256
554
|
}
|
|
555
|
+
break;
|
|
257
556
|
}
|
|
258
557
|
}
|
|
259
|
-
// Environment variables override file values so
|
|
260
|
-
//
|
|
558
|
+
// Environment variables override file values, so an instance can be
|
|
559
|
+
// retargeted (ports, engine data dir, region) without touching the file.
|
|
261
560
|
this.loadFromEnv();
|
|
262
561
|
}
|
|
263
562
|
loadFromEnv() {
|
|
@@ -266,33 +565,6 @@ export class ConfigManager {
|
|
|
266
565
|
/* istanbul ignore next: the enclosing if guarantees one of these is truthy, so the `|| ''` fallback is unreachable */
|
|
267
566
|
this.config.serverPort = parseInt(process.env.LSS_DASHBOARD_PORT || process.env.PORT || '', 10);
|
|
268
567
|
}
|
|
269
|
-
if (process.env.LSS_LOCALSTACK_PORT) {
|
|
270
|
-
this.config.localstackPort = parseInt(process.env.LSS_LOCALSTACK_PORT, 10);
|
|
271
|
-
}
|
|
272
|
-
if (process.env.LSS_LOCALSTACK_ENDPOINT) {
|
|
273
|
-
this.config.localstackEndpoint = process.env.LSS_LOCALSTACK_ENDPOINT;
|
|
274
|
-
}
|
|
275
|
-
if (process.env.LSS_LOCALSTACK_MODE) {
|
|
276
|
-
const mode = process.env.LSS_LOCALSTACK_MODE.toLowerCase();
|
|
277
|
-
if (mode === 'managed' || mode === 'external') {
|
|
278
|
-
this.config.mode = mode;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (process.env.LSS_LOCALSTACK_EDITION) {
|
|
282
|
-
const edition = process.env.LSS_LOCALSTACK_EDITION.toLowerCase();
|
|
283
|
-
if (edition === 'community' || edition === 'pro') {
|
|
284
|
-
this.config.localstackEdition = edition;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (process.env.LSS_LOCALSTACK_VERSION) {
|
|
288
|
-
this.config.localstackVersion = process.env.LSS_LOCALSTACK_VERSION;
|
|
289
|
-
}
|
|
290
|
-
if (process.env.LSS_LOCALSTACK_IMAGE) {
|
|
291
|
-
this.config.localstackImage = process.env.LSS_LOCALSTACK_IMAGE;
|
|
292
|
-
}
|
|
293
|
-
if (process.env.LOCALSTACK_AUTH_TOKEN) {
|
|
294
|
-
this.config.localstackAuthToken = process.env.LOCALSTACK_AUTH_TOKEN;
|
|
295
|
-
}
|
|
296
568
|
if (process.env.LSS_ENABLE_DYNAMO_PROXY) {
|
|
297
569
|
this.config.enableDynamoProxy = process.env.LSS_ENABLE_DYNAMO_PROXY === 'true' || process.env.LSS_ENABLE_DYNAMO_PROXY === '1';
|
|
298
570
|
}
|
|
@@ -302,9 +574,6 @@ export class ConfigManager {
|
|
|
302
574
|
if (process.env.AWS_REGION) {
|
|
303
575
|
this.config.region = process.env.AWS_REGION;
|
|
304
576
|
}
|
|
305
|
-
if (process.env.LSS_SERVICES) {
|
|
306
|
-
this.config.services = process.env.LSS_SERVICES.split(',');
|
|
307
|
-
}
|
|
308
577
|
if (process.env.LSS_PERSISTENCE) {
|
|
309
578
|
this.config.persistence = process.env.LSS_PERSISTENCE === 'true' || process.env.LSS_PERSISTENCE === '1';
|
|
310
579
|
}
|
|
@@ -350,61 +619,32 @@ export class ConfigManager {
|
|
|
350
619
|
invokeHost: process.env.LSS_INVOKE_HOST,
|
|
351
620
|
};
|
|
352
621
|
}
|
|
353
|
-
if (process.env.LSS_ENGINE) {
|
|
354
|
-
const engine = process.env.LSS_ENGINE.toLowerCase();
|
|
355
|
-
if (engine === 'localstack' || engine === 'self') {
|
|
356
|
-
this.config.engine = engine;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
622
|
if (process.env.LSS_ENGINE_PORT) {
|
|
360
623
|
this.config.selfEngine = {
|
|
361
624
|
...this.config.selfEngine,
|
|
362
625
|
port: parseInt(process.env.LSS_ENGINE_PORT, 10),
|
|
363
626
|
};
|
|
364
627
|
}
|
|
628
|
+
// Give every instance its own engine state without editing a config file:
|
|
629
|
+
// `LSS_ENGINE_DATA_DIR=... LSS_ENGINE_PORT=... lss start` is enough to run a
|
|
630
|
+
// second stack (a test run, a second checkout) beside the dev one.
|
|
631
|
+
if (process.env.LSS_ENGINE_DATA_DIR) {
|
|
632
|
+
this.config.selfEngine = {
|
|
633
|
+
...this.config.selfEngine,
|
|
634
|
+
dataDir: process.env.LSS_ENGINE_DATA_DIR,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
365
637
|
}
|
|
366
638
|
getConfig() {
|
|
367
639
|
return this.config;
|
|
368
640
|
}
|
|
369
641
|
getServerPort() {
|
|
370
|
-
return this.config.serverPort ?? (parseInt(process.env.PORT || '', 10) ||
|
|
642
|
+
return this.config.serverPort ?? (parseInt(process.env.PORT || '', 10) || DEFAULT_PORT);
|
|
371
643
|
}
|
|
372
644
|
// Alias for backward compatibility
|
|
373
645
|
getDashboardPort() {
|
|
374
646
|
return this.getServerPort();
|
|
375
647
|
}
|
|
376
|
-
getLocalStackPort() {
|
|
377
|
-
return this.config.localstackPort ?? 4566;
|
|
378
|
-
}
|
|
379
|
-
getLocalStackEndpoint() {
|
|
380
|
-
if (this.config.localstackEndpoint) {
|
|
381
|
-
return this.config.localstackEndpoint;
|
|
382
|
-
}
|
|
383
|
-
const port = this.getLocalStackPort();
|
|
384
|
-
return `http://localhost:${port}`;
|
|
385
|
-
}
|
|
386
|
-
getMode() {
|
|
387
|
-
return this.config.mode ?? 'managed';
|
|
388
|
-
}
|
|
389
|
-
isExternal() {
|
|
390
|
-
return this.getMode() === 'external';
|
|
391
|
-
}
|
|
392
|
-
getLocalStackEdition() {
|
|
393
|
-
return this.config.localstackEdition ?? 'community';
|
|
394
|
-
}
|
|
395
|
-
getLocalStackVersion() {
|
|
396
|
-
return this.config.localstackVersion ?? 'latest';
|
|
397
|
-
}
|
|
398
|
-
getLocalStackImage() {
|
|
399
|
-
if (this.config.localstackImage) {
|
|
400
|
-
return this.config.localstackImage;
|
|
401
|
-
}
|
|
402
|
-
const repo = this.getLocalStackEdition() === 'pro' ? 'localstack/localstack-pro' : 'localstack/localstack';
|
|
403
|
-
return `${repo}:${this.getLocalStackVersion()}`;
|
|
404
|
-
}
|
|
405
|
-
getLocalStackAuthToken() {
|
|
406
|
-
return this.config.localstackAuthToken;
|
|
407
|
-
}
|
|
408
648
|
isEnableDynamoProxy() {
|
|
409
649
|
if (this.config.enableDynamoProxy !== undefined) {
|
|
410
650
|
return this.config.enableDynamoProxy;
|
|
@@ -418,9 +658,6 @@ export class ConfigManager {
|
|
|
418
658
|
getRegion() {
|
|
419
659
|
return this.config.region ?? (process.env.AWS_REGION || 'us-east-1');
|
|
420
660
|
}
|
|
421
|
-
getServices() {
|
|
422
|
-
return this.config.services ?? ['dynamodb', 'sqs', 'sns', 's3', 'lambda', 'events'];
|
|
423
|
-
}
|
|
424
661
|
isPersistence() {
|
|
425
662
|
if (this.config.persistence !== undefined) {
|
|
426
663
|
return this.config.persistence;
|
|
@@ -468,11 +705,7 @@ export class ConfigManager {
|
|
|
468
705
|
* as the global baseline.
|
|
469
706
|
*/
|
|
470
707
|
getPackageConfigForService(servicePath) {
|
|
471
|
-
const
|
|
472
|
-
const baseName = path.basename(servicePath);
|
|
473
|
-
const configDir = path.resolve(this.configPath ? path.dirname(this.configPath) : process.cwd());
|
|
474
|
-
const relPath = path.relative(configDir, servicePath).split(path.sep).join('/');
|
|
475
|
-
const perService = map[relPath] ?? map[baseName] ?? {};
|
|
708
|
+
const perService = this.lookupServiceEntry(this.config.servicePackaging, servicePath);
|
|
476
709
|
return {
|
|
477
710
|
command: perService.packageCommand ?? this.getPackageCommand(),
|
|
478
711
|
args: [...(this.config.packageArgs ?? []), ...(perService.packageArgs ?? [])],
|
|
@@ -480,6 +713,40 @@ export class ConfigManager {
|
|
|
480
713
|
timeoutMs: perService.packageTimeoutMs ?? this.getPackageTimeoutMs(),
|
|
481
714
|
};
|
|
482
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* The keys a `servicePackaging`/`serviceRuntime` entry may be written under,
|
|
718
|
+
* most specific first: the path relative to the config file's directory, the
|
|
719
|
+
* path relative to the project root, then the directory basename.
|
|
720
|
+
*
|
|
721
|
+
* The two relative forms are usually the same string — but not always, and
|
|
722
|
+
* the difference used to silently drop overrides: `getProjectRoot()`
|
|
723
|
+
* realpath-resolves (and falls back to the cwd for a home-directory config),
|
|
724
|
+
* while a raw `dirname(configPath)` does not. A checkout reached through a
|
|
725
|
+
* symlink, or a config loaded from `~`, made the key the dashboard writes
|
|
726
|
+
* (project-root relative, from the scan) unreachable by the lookup. Both
|
|
727
|
+
* spellings resolve here so a written override always applies.
|
|
728
|
+
*/
|
|
729
|
+
serviceEntryKeys(servicePath) {
|
|
730
|
+
const target = realpathOrSelf(path.resolve(servicePath));
|
|
731
|
+
const bases = [
|
|
732
|
+
this.configPath ? realpathOrSelf(path.resolve(path.dirname(this.configPath))) : null,
|
|
733
|
+
this.getProjectRoot(),
|
|
734
|
+
];
|
|
735
|
+
const keys = bases
|
|
736
|
+
.filter((base) => base !== null)
|
|
737
|
+
.map(base => path.relative(base, target).split(path.sep).join('/'))
|
|
738
|
+
.filter(Boolean);
|
|
739
|
+
keys.push(path.basename(target));
|
|
740
|
+
return [...new Set(keys)];
|
|
741
|
+
}
|
|
742
|
+
lookupServiceEntry(map, servicePath) {
|
|
743
|
+
const entries = map ?? {};
|
|
744
|
+
for (const key of this.serviceEntryKeys(servicePath)) {
|
|
745
|
+
if (Object.prototype.hasOwnProperty.call(entries, key))
|
|
746
|
+
return entries[key];
|
|
747
|
+
}
|
|
748
|
+
return {};
|
|
749
|
+
}
|
|
483
750
|
isLambdaRuntimeEnabled() {
|
|
484
751
|
return this.config.lambdaRuntime?.enabled ?? true;
|
|
485
752
|
}
|
|
@@ -489,13 +756,36 @@ export class ConfigManager {
|
|
|
489
756
|
getInvokePortOffset() {
|
|
490
757
|
return this.config.lambdaRuntime?.invokePortOffset ?? 10000;
|
|
491
758
|
}
|
|
759
|
+
// Defer forking a service's worker to its first invocation. On by default:
|
|
760
|
+
// one Node process per registered service is ~48 MB, which a 40-service
|
|
761
|
+
// monorepo pays entirely up front for handlers it may never call.
|
|
762
|
+
isLambdaRuntimeLazy() {
|
|
763
|
+
return this.config.lambdaRuntime?.lazy ?? true;
|
|
764
|
+
}
|
|
765
|
+
// A worker only stays resident while it is being used. 0 disables unloading.
|
|
766
|
+
getLambdaIdleTimeoutMs() {
|
|
767
|
+
const raw = this.config.lambdaRuntime?.idleTimeoutMs;
|
|
768
|
+
if (raw === undefined)
|
|
769
|
+
return 60_000;
|
|
770
|
+
return typeof raw === 'number' && raw > 0 ? raw : 0;
|
|
771
|
+
}
|
|
772
|
+
// Ceiling on resident workers. Default: one per GB of system RAM, clamped to
|
|
773
|
+
// 2..12 — the point is that the host's memory is bounded by how many services
|
|
774
|
+
// are in flight, never by how many are registered. 0 disables the ceiling.
|
|
775
|
+
getLambdaMaxWarmWorkers() {
|
|
776
|
+
const raw = this.config.lambdaRuntime?.maxWarmWorkers;
|
|
777
|
+
if (typeof raw === 'number' && raw >= 0)
|
|
778
|
+
return Math.floor(raw);
|
|
779
|
+
const gib = Math.floor(os.totalmem() / (1024 ** 3));
|
|
780
|
+
return Math.max(2, Math.min(12, gib));
|
|
781
|
+
}
|
|
492
782
|
getInvokeHost() {
|
|
493
783
|
if (this.config.lambdaRuntime?.invokeHost) {
|
|
494
784
|
return this.config.lambdaRuntime.invokeHost;
|
|
495
785
|
}
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
return
|
|
786
|
+
// Nothing runs inside a container, so callbacks into the invoke listeners
|
|
787
|
+
// resolve on the loopback directly.
|
|
788
|
+
return '127.0.0.1';
|
|
499
789
|
}
|
|
500
790
|
/**
|
|
501
791
|
* Resolve the effective runtime settings for one service, merging the global
|
|
@@ -505,11 +795,7 @@ export class ConfigManager {
|
|
|
505
795
|
* (true for source, false for artifact).
|
|
506
796
|
*/
|
|
507
797
|
getRuntimeConfigForService(servicePath) {
|
|
508
|
-
const
|
|
509
|
-
const baseName = path.basename(servicePath);
|
|
510
|
-
const configDir = path.resolve(this.configPath ? path.dirname(this.configPath) : process.cwd());
|
|
511
|
-
const relPath = path.relative(configDir, servicePath).split(path.sep).join('/');
|
|
512
|
-
const perService = map[relPath] ?? map[baseName] ?? {};
|
|
798
|
+
const perService = this.lookupServiceEntry(this.config.serviceRuntime, servicePath);
|
|
513
799
|
return {
|
|
514
800
|
enabled: perService.enabled ?? this.isLambdaRuntimeEnabled(),
|
|
515
801
|
execution: perService.execution ?? this.getLambdaExecutionMode(),
|
|
@@ -518,19 +804,13 @@ export class ConfigManager {
|
|
|
518
804
|
invokePort: perService.invokePort,
|
|
519
805
|
};
|
|
520
806
|
}
|
|
521
|
-
getEngineKind() {
|
|
522
|
-
return this.config.engine ?? 'localstack';
|
|
523
|
-
}
|
|
524
|
-
isSelfEngine() {
|
|
525
|
-
return this.getEngineKind() === 'self';
|
|
526
|
-
}
|
|
527
807
|
getSelfEngineConfig() {
|
|
528
808
|
const selfEngine = this.config.selfEngine ?? {};
|
|
529
809
|
const stateDir = this.getStateDir();
|
|
530
810
|
const rawDataDir = selfEngine.dataDir
|
|
531
|
-
?? (stateDir ? path.join(stateDir, 'engine') :
|
|
811
|
+
?? (stateDir ? path.join(stateDir, 'engine') : this.homeStateDir('engine'));
|
|
532
812
|
return {
|
|
533
|
-
port: selfEngine.port ??
|
|
813
|
+
port: selfEngine.port ?? DEFAULT_PORT,
|
|
534
814
|
dataDir: path.isAbsolute(rawDataDir) ? rawDataDir : path.resolve(process.cwd(), rawDataDir),
|
|
535
815
|
account: selfEngine.account ?? '000000000000',
|
|
536
816
|
idleUnloadMs: selfEngine.idleUnloadMs ?? 300000,
|
|
@@ -541,27 +821,27 @@ export class ConfigManager {
|
|
|
541
821
|
region: this.getRegion(),
|
|
542
822
|
};
|
|
543
823
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return
|
|
550
|
-
enabled,
|
|
551
|
-
port,
|
|
552
|
-
endpoint: `http://localhost:${port}`,
|
|
553
|
-
// Mirrors the self-engine default so both persistence roots sit side by
|
|
554
|
-
// side (<stateDir>/aoss for test isolation, else ~/.lss/aoss).
|
|
555
|
-
dataDir: stateDir ? path.join(stateDir, 'aoss') : path.join(os.homedir(), '.lss', 'aoss'),
|
|
556
|
-
};
|
|
824
|
+
// Fallback state root when the config declares no stateDir. Scoped to this
|
|
825
|
+
// project: a flat ~/.lss/<kind> meant every project on the machine shared one
|
|
826
|
+
// set of DynamoDB tables, queues and OpenSearch indices, so opening a second
|
|
827
|
+
// repo silently showed the first one's data.
|
|
828
|
+
homeStateDir(kind) {
|
|
829
|
+
return path.join(os.homedir(), '.lss', 'projects', projectCacheSegment(this.getProjectRoot()), kind);
|
|
557
830
|
}
|
|
558
831
|
// The AWS endpoint every SDK-based consumer (provisioner, explorers, seeds)
|
|
559
|
-
//
|
|
832
|
+
// targets. Reads the port directly rather than through getSelfEngineConfig(),
|
|
833
|
+
// which would also resolve the data dir — needless work for a URL, and it
|
|
834
|
+
// drags the project-root lookup into every caller that only wants an endpoint.
|
|
560
835
|
getEngineEndpoint() {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
836
|
+
return `http://localhost:${this.config.selfEngine?.port ?? DEFAULT_PORT}`;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* True when the orchestrator and the engine share one listener — the default,
|
|
840
|
+
* since both ports default to the same value. Give them different values to
|
|
841
|
+
* go back to two listeners.
|
|
842
|
+
*/
|
|
843
|
+
isSingleListener() {
|
|
844
|
+
return this.getServerPort() === (this.config.selfEngine?.port ?? DEFAULT_PORT);
|
|
565
845
|
}
|
|
566
846
|
getConfigPath() {
|
|
567
847
|
return this.configPath;
|
|
@@ -640,26 +920,16 @@ export class ConfigManager {
|
|
|
640
920
|
// (per request/registration/seed run) and follows a reload immediately.
|
|
641
921
|
resolvedBootValues() {
|
|
642
922
|
const selfEngine = this.getSelfEngineConfig();
|
|
643
|
-
const aossSidecar = this.getAossSidecarConfig();
|
|
644
923
|
return {
|
|
645
924
|
serverPort: this.getServerPort(),
|
|
646
|
-
localstackPort: this.getLocalStackPort(),
|
|
647
|
-
localstackEndpoint: this.getLocalStackEndpoint(),
|
|
648
|
-
mode: this.getMode(),
|
|
649
|
-
localstackEdition: this.getLocalStackEdition(),
|
|
650
|
-
localstackVersion: this.getLocalStackVersion(),
|
|
651
|
-
localstackImage: this.getLocalStackImage(),
|
|
652
925
|
enableDynamoProxy: this.isEnableDynamoProxy(),
|
|
653
926
|
dynamoProxyPort: this.getDynamoProxyPort(),
|
|
654
927
|
region: this.getRegion(),
|
|
655
|
-
services: this.getServices(),
|
|
656
928
|
persistence: this.isPersistence(),
|
|
657
929
|
debug: this.isDebug(),
|
|
658
930
|
stateDir: this.getStateDir(),
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
// stateDir-derived dataDir (aoss) are tracked under their own keys above,
|
|
662
|
-
// so one change never flags two entries.
|
|
931
|
+
// Only the block's OWN settings: region/persistence are tracked under
|
|
932
|
+
// their own keys above, so one change never flags two entries.
|
|
663
933
|
selfEngine: {
|
|
664
934
|
port: selfEngine.port,
|
|
665
935
|
dataDir: selfEngine.dataDir,
|
|
@@ -669,7 +939,6 @@ export class ConfigManager {
|
|
|
669
939
|
fsync: selfEngine.fsync,
|
|
670
940
|
fallbackEndpoint: selfEngine.fallbackEndpoint,
|
|
671
941
|
},
|
|
672
|
-
aossSidecar: { enabled: aossSidecar.enabled, port: aossSidecar.port },
|
|
673
942
|
};
|
|
674
943
|
}
|
|
675
944
|
static diffKeys(before, after) {
|
|
@@ -726,7 +995,9 @@ export class ConfigManager {
|
|
|
726
995
|
errors.push(`"${key}" cannot be edited via the API — set it in the config file or environment directly`);
|
|
727
996
|
continue;
|
|
728
997
|
}
|
|
729
|
-
const spec = EDITABLE_CONFIG_KEYS
|
|
998
|
+
const spec = Object.prototype.hasOwnProperty.call(EDITABLE_CONFIG_KEYS, key)
|
|
999
|
+
? EDITABLE_CONFIG_KEYS[key]
|
|
1000
|
+
: undefined;
|
|
730
1001
|
if (!spec) {
|
|
731
1002
|
errors.push(`unknown config key "${key}"`);
|
|
732
1003
|
continue;
|
|
@@ -763,6 +1034,10 @@ export class ConfigManager {
|
|
|
763
1034
|
else if (EDITABLE_CONFIG_KEYS[key].kind === 'object') {
|
|
764
1035
|
// One-level merge so a partial block edit (e.g. branding.title) never
|
|
765
1036
|
// drops sibling settings the UI does not round-trip (branding.logo).
|
|
1037
|
+
// Map-shaped blocks (serviceRuntime/servicePackaging) merge one level
|
|
1038
|
+
// deeper for the same reason: onboarding sending { apiPort } for one
|
|
1039
|
+
// service must not drop that entry's execution/packageEnv siblings.
|
|
1040
|
+
const perEntry = Boolean(EDITABLE_CONFIG_KEYS[key].entrySubKeys);
|
|
766
1041
|
const existing = isPlainObject(fileConfig[key])
|
|
767
1042
|
? fileConfig[key]
|
|
768
1043
|
: {};
|
|
@@ -771,6 +1046,29 @@ export class ConfigManager {
|
|
|
771
1046
|
if (subValue === null) {
|
|
772
1047
|
delete merged[subKey];
|
|
773
1048
|
}
|
|
1049
|
+
else if (perEntry && isPlainObject(subValue)) {
|
|
1050
|
+
const entryExisting = isPlainObject(merged[subKey])
|
|
1051
|
+
? merged[subKey]
|
|
1052
|
+
: {};
|
|
1053
|
+
const entryMerged = { ...entryExisting };
|
|
1054
|
+
for (const [entryKey, entryValue] of Object.entries(subValue)) {
|
|
1055
|
+
if (entryValue === null) {
|
|
1056
|
+
delete entryMerged[entryKey];
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
entryMerged[entryKey] = entryValue;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// An entry left with no settings carries no meaning and would
|
|
1063
|
+
// shadow a basename-keyed entry for the same service (an empty
|
|
1064
|
+
// object is truthy), so it is dropped rather than written.
|
|
1065
|
+
if (Object.keys(entryMerged).length === 0) {
|
|
1066
|
+
delete merged[subKey];
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
merged[subKey] = entryMerged;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
774
1072
|
else {
|
|
775
1073
|
merged[subKey] = subValue;
|
|
776
1074
|
}
|
|
@@ -799,29 +1097,17 @@ export class ConfigManager {
|
|
|
799
1097
|
printSummary() {
|
|
800
1098
|
console.log('\n📋 Configuration Summary:');
|
|
801
1099
|
console.log(` Server Port: ${this.getServerPort()} (http://localhost:${this.getServerPort()})`);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
console.log(` Self Engine
|
|
807
|
-
if (selfEngine.fallbackEndpoint) {
|
|
808
|
-
console.log(` Self Engine Fallback: ${selfEngine.fallbackEndpoint}`);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
console.log(` LocalStack Mode: ${this.getMode()}`);
|
|
813
|
-
console.log(` LocalStack Port: ${this.getLocalStackPort()} (${this.getLocalStackEndpoint()})`);
|
|
814
|
-
if (!this.isExternal()) {
|
|
815
|
-
console.log(` LocalStack Image: ${this.getLocalStackImage()} (${this.getLocalStackEdition()})`);
|
|
816
|
-
console.log(` LocalStack Auth Token: ${this.getLocalStackAuthToken() ? 'set' : 'not set'}`);
|
|
817
|
-
}
|
|
1100
|
+
const selfEngine = this.getSelfEngineConfig();
|
|
1101
|
+
console.log(` Self Engine Port: ${selfEngine.port} (http://localhost:${selfEngine.port})`);
|
|
1102
|
+
console.log(` Self Engine Data Dir: ${selfEngine.dataDir}`);
|
|
1103
|
+
if (selfEngine.fallbackEndpoint) {
|
|
1104
|
+
console.log(` Self Engine Fallback: ${selfEngine.fallbackEndpoint}`);
|
|
818
1105
|
}
|
|
819
1106
|
console.log(` DynamoDB Proxy Enabled: ${this.isEnableDynamoProxy()}`);
|
|
820
1107
|
if (this.isEnableDynamoProxy()) {
|
|
821
1108
|
console.log(` DynamoDB Proxy Port: ${this.getDynamoProxyPort()}`);
|
|
822
1109
|
}
|
|
823
1110
|
console.log(` AWS Region: ${this.getRegion()}`);
|
|
824
|
-
console.log(` Services: ${this.getServices().join(', ')}`);
|
|
825
1111
|
console.log(` Persistence: ${this.isPersistence()}`);
|
|
826
1112
|
console.log(` Seeds Dir: ${this.getSeedsDir()}`);
|
|
827
1113
|
const secretSeedCount = Object.keys(this.getSecretSeeds()).length;
|