local-serverless-stack 1.0.1 → 1.1.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 +125 -3
- package/README.md +75 -26
- package/bin/cli.js +81 -15
- package/bin/i18n.js +30 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/namespaces/services.d.ts +46 -0
- package/dist/client/namespaces/services.d.ts.map +1 -1
- package/dist/client/namespaces/services.js +1 -0
- package/dist/client/namespaces/services.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +17 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/server/engine/backends/self-backend.d.ts +26 -1
- package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
- package/dist/server/engine/backends/self-backend.js +104 -11
- package/dist/server/engine/backends/self-backend.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.js +12 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts +3 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts.map +1 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js +78 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js.map +1 -0
- package/dist/server/engine/emulators/dynamodb/index.d.ts +11 -0
- package/dist/server/engine/emulators/dynamodb/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/index.js +101 -7
- package/dist/server/engine/emulators/dynamodb/index.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/query.d.ts +1 -0
- package/dist/server/engine/emulators/dynamodb/query.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/query.js +34 -6
- package/dist/server/engine/emulators/dynamodb/query.js.map +1 -1
- package/dist/server/engine/emulators/s3/index.d.ts +10 -0
- package/dist/server/engine/emulators/s3/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/s3/index.js +369 -25
- package/dist/server/engine/emulators/s3/index.js.map +1 -1
- package/dist/server/engine/emulators/sqs/index.d.ts +2 -0
- package/dist/server/engine/emulators/sqs/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/sqs/index.js +70 -7
- package/dist/server/engine/emulators/sqs/index.js.map +1 -1
- package/dist/server/engine/emulators/sqs/queue.d.ts +2 -1
- package/dist/server/engine/emulators/sqs/queue.d.ts.map +1 -1
- package/dist/server/engine/emulators/sqs/queue.js +68 -35
- package/dist/server/engine/emulators/sqs/queue.js.map +1 -1
- package/dist/server/engine/emulators/ssm/index.d.ts +31 -0
- package/dist/server/engine/emulators/ssm/index.d.ts.map +1 -0
- package/dist/server/engine/emulators/ssm/index.js +670 -0
- package/dist/server/engine/emulators/ssm/index.js.map +1 -0
- package/dist/server/engine/http/errors.d.ts +1 -0
- package/dist/server/engine/http/errors.d.ts.map +1 -1
- package/dist/server/engine/http/errors.js +13 -5
- package/dist/server/engine/http/errors.js.map +1 -1
- package/dist/server/engine/http/is-aws-request.d.ts.map +1 -1
- package/dist/server/engine/http/is-aws-request.js +13 -3
- package/dist/server/engine/http/is-aws-request.js.map +1 -1
- package/dist/server/engine/http/protocols/aws-json.d.ts +1 -0
- package/dist/server/engine/http/protocols/aws-json.d.ts.map +1 -1
- package/dist/server/engine/http/protocols/aws-json.js +13 -0
- package/dist/server/engine/http/protocols/aws-json.js.map +1 -1
- package/dist/server/engine/http/reset.d.ts +4 -0
- package/dist/server/engine/http/reset.d.ts.map +1 -0
- package/dist/server/engine/http/reset.js +17 -0
- package/dist/server/engine/http/reset.js.map +1 -0
- package/dist/server/engine/http/router.d.ts +1 -0
- package/dist/server/engine/http/router.d.ts.map +1 -1
- package/dist/server/engine/http/router.js +185 -34
- package/dist/server/engine/http/router.js.map +1 -1
- package/dist/server/engine/types.d.ts +2 -1
- package/dist/server/engine/types.d.ts.map +1 -1
- package/dist/server/routes/apis.d.ts.map +1 -1
- package/dist/server/routes/apis.js +6 -0
- package/dist/server/routes/apis.js.map +1 -1
- package/dist/server/routes/config.d.ts.map +1 -1
- package/dist/server/routes/config.js +19 -1
- package/dist/server/routes/config.js.map +1 -1
- package/dist/server/routes/services.d.ts.map +1 -1
- package/dist/server/routes/services.js +49 -1
- package/dist/server/routes/services.js.map +1 -1
- package/dist/server/runtime/runtime-worker.js +33 -3
- package/dist/server/runtime/runtime-worker.js.map +1 -1
- package/dist/server/services/api-gateway-events.d.ts +14 -0
- package/dist/server/services/api-gateway-events.d.ts.map +1 -1
- package/dist/server/services/api-gateway-events.js +114 -8
- package/dist/server/services/api-gateway-events.js.map +1 -1
- package/dist/server/services/artifact-staleness.d.ts +20 -0
- package/dist/server/services/artifact-staleness.d.ts.map +1 -0
- package/dist/server/services/artifact-staleness.js +115 -0
- package/dist/server/services/artifact-staleness.js.map +1 -0
- package/dist/server/services/authorizer-service.d.ts +34 -5
- package/dist/server/services/authorizer-service.d.ts.map +1 -1
- package/dist/server/services/authorizer-service.js +0 -0
- package/dist/server/services/authorizer-service.js.map +1 -1
- package/dist/server/services/cfn-intrinsics.d.ts +63 -0
- package/dist/server/services/cfn-intrinsics.d.ts.map +1 -0
- package/dist/server/services/cfn-intrinsics.js +403 -0
- package/dist/server/services/cfn-intrinsics.js.map +1 -0
- package/dist/server/services/cfn-physical-values.d.ts +51 -0
- package/dist/server/services/cfn-physical-values.d.ts.map +1 -0
- package/dist/server/services/cfn-physical-values.js +186 -0
- package/dist/server/services/cfn-physical-values.js.map +1 -0
- package/dist/server/services/cloudformation-parser.d.ts +26 -3
- package/dist/server/services/cloudformation-parser.d.ts.map +1 -1
- package/dist/server/services/cloudformation-parser.js +76 -4
- package/dist/server/services/cloudformation-parser.js.map +1 -1
- package/dist/server/services/config-manager.d.ts +24 -2
- package/dist/server/services/config-manager.d.ts.map +1 -1
- package/dist/server/services/config-manager.js +60 -7
- package/dist/server/services/config-manager.js.map +1 -1
- package/dist/server/services/gateway-manager.d.ts.map +1 -1
- package/dist/server/services/gateway-manager.js +52 -20
- package/dist/server/services/gateway-manager.js.map +1 -1
- package/dist/server/services/lambda-runtime-manager.d.ts +1 -0
- package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
- package/dist/server/services/lambda-runtime-manager.js +36 -14
- package/dist/server/services/lambda-runtime-manager.js.map +1 -1
- package/dist/server/services/raw-api-assembler.d.ts +1 -1
- package/dist/server/services/raw-api-assembler.d.ts.map +1 -1
- package/dist/server/services/raw-api-assembler.js +18 -8
- package/dist/server/services/raw-api-assembler.js.map +1 -1
- package/dist/server/services/resource-provisioner.d.ts +12 -0
- package/dist/server/services/resource-provisioner.d.ts.map +1 -1
- package/dist/server/services/resource-provisioner.js +128 -13
- package/dist/server/services/resource-provisioner.js.map +1 -1
- package/dist/server/services/serverless-state-parser.d.ts +45 -2
- package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
- package/dist/server/services/serverless-state-parser.js +217 -29
- package/dist/server/services/serverless-state-parser.js.map +1 -1
- package/dist/server/services/service-graph.d.ts +76 -0
- package/dist/server/services/service-graph.d.ts.map +1 -0
- package/dist/server/services/service-graph.js +914 -0
- package/dist/server/services/service-graph.js.map +1 -0
- package/dist/server/services/service-registrar.d.ts +4 -0
- package/dist/server/services/service-registrar.d.ts.map +1 -1
- package/dist/server/services/service-registrar.js +129 -55
- package/dist/server/services/service-registrar.js.map +1 -1
- package/dist/server/services/source-watcher.d.ts +1 -0
- package/dist/server/services/source-watcher.d.ts.map +1 -1
- package/dist/server/services/source-watcher.js +19 -1
- package/dist/server/services/source-watcher.js.map +1 -1
- package/dist/server/services/worker-runtime.d.ts +82 -0
- package/dist/server/services/worker-runtime.d.ts.map +1 -0
- package/dist/server/services/worker-runtime.js +303 -0
- package/dist/server/services/worker-runtime.js.map +1 -0
- package/dist/ui/assets/{ApisPage-CTOcNna-.js → ApisPage-BkW-QZ0X.js} +1 -1
- package/dist/ui/assets/BucketDetailPage-CJwTQJh7.js +1 -0
- package/dist/ui/assets/BucketsPage-B1xPVhZC.js +1 -0
- package/dist/ui/assets/{DynamoPage-BnFFKnIU.js → DynamoPage-DvbpwZDc.js} +1 -1
- package/dist/ui/assets/DynamoTablePage-xn3Lq3so.js +3 -0
- package/dist/ui/assets/LambdaDetailPage-BhUyO77H.js +4 -0
- package/dist/ui/assets/{LambdasPage-Bg-MWFBb.js → LambdasPage-CUF_4rOH.js} +1 -1
- package/dist/ui/assets/OnboardingPage-U49THUzu.js +1 -0
- package/dist/ui/assets/OpenSearchCollectionPage-BJTVNCq4.js +1 -0
- package/dist/ui/assets/OpenSearchPage-4s-egkB9.js +1 -0
- package/dist/ui/assets/OverviewPage-DJeJQktG.js +1 -0
- package/dist/ui/assets/{OverviewPage-CSWY71vZ.css → OverviewPage-DjQETmb_.css} +1 -1
- package/dist/ui/assets/QueueDetailPage-DWsy5Yt8.js +1 -0
- package/dist/ui/assets/QueuesPage-BZJSqman.js +1 -0
- package/dist/ui/assets/SecretsPage-YC9ua54G.js +1 -0
- package/dist/ui/assets/ServiceDetailPage-Cum5frP4.js +2 -0
- package/dist/ui/assets/ServicesPage-IpaPDuqx.js +2 -0
- package/dist/ui/assets/SettingsPage-BiCRU8O6.js +1 -0
- package/dist/ui/assets/index-BDlrAJhi.css +1 -0
- package/dist/ui/assets/index-BouKSS43.js +38 -0
- package/dist/ui/assets/resourceIcons-DZs28nbs.js +1 -0
- package/dist/ui/index.html +5 -2
- package/package.json +1 -1
- package/dist/ui/assets/BucketDetailPage-BXW8-FrE.js +0 -1
- package/dist/ui/assets/BucketsPage-DY_ITepb.js +0 -1
- package/dist/ui/assets/DynamoTablePage-BLCuyJHU.js +0 -3
- package/dist/ui/assets/LambdaDetailPage-D4rZNLRT.js +0 -4
- package/dist/ui/assets/OnboardingPage-nnl313Fm.js +0 -1
- package/dist/ui/assets/OpenSearchCollectionPage-Dn15lH_F.js +0 -1
- package/dist/ui/assets/OpenSearchPage-CgD5isok.js +0 -1
- package/dist/ui/assets/OverviewPage-m5sUr8IN.js +0 -1
- package/dist/ui/assets/QueueDetailPage-Ch_FDDoN.js +0 -1
- package/dist/ui/assets/QueuesPage-C_yDuGDu.js +0 -1
- package/dist/ui/assets/SecretsPage-BSGMSoqy.js +0 -1
- package/dist/ui/assets/ServiceDetailPage-6KyohyjI.js +0 -2
- package/dist/ui/assets/ServicesPage-BbSetOMo.js +0 -2
- package/dist/ui/assets/SettingsPage-BgF1jFr6.js +0 -1
- package/dist/ui/assets/index-D627ZyYU.css +0 -1
- package/dist/ui/assets/index-DuSPnI19.js +0 -37
- package/dist/ui/assets/resourceIcons-baecADXA.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,126 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.1.0] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
**Templates that run on AWS now run on LSS without an adapter.** A real serverless stack names its
|
|
13
|
+
resources with `!Sub '${AWS::StackName}-orders'`, hands them to functions as `!Ref OrdersTable`,
|
|
14
|
+
points its authorizer at `!GetAtt AuthorizerLambdaFunction.Arn` and leans on S3, SQS and API
|
|
15
|
+
Gateway behaving exactly as AWS does. Each of those used to need a workaround outside LSS; this
|
|
16
|
+
release closes them in the engine, the template pipeline, the Lambda runtime and the gateway.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **SSM Parameter Store** in the self engine (AWS JSON 1.1, `AmazonSSM.*`): `PutParameter`
|
|
21
|
+
(`String`/`StringList`/`SecureString`, `Overwrite` versioning, `AllowedPattern`, tiers, `DataType`,
|
|
22
|
+
AWS name and hierarchy rules), `GetParameter(s)` with `name:version` / `name:label` selectors,
|
|
23
|
+
`GetParametersByPath` (`Recursive`, filters, paging), `DescribeParameters`, `GetParameterHistory`,
|
|
24
|
+
`LabelParameterVersion`, `DeleteParameter(s)` and tagging. `AWS::SSM::Parameter` is provisioned at
|
|
25
|
+
registration. `SecureString` is not encrypted: without `WithDecryption` an opaque deterministic
|
|
26
|
+
token stands in for the ciphertext.
|
|
27
|
+
- **CloudFormation intrinsics.** A static pass resolves `Parameters` (defaults), pseudo parameters,
|
|
28
|
+
`Mappings`, `Conditions` and `Fn::Sub` / `Join` / `Select` / `Split` / `If` / `FindInMap` /
|
|
29
|
+
`GetAZs` / `Base64` / `ImportValue` before any resource is read; a resource whose `Condition` is
|
|
30
|
+
false is not created. After provisioning, `Ref`/`GetAtt` in function environments become physical
|
|
31
|
+
values — table name, queue URL, stream ARN, secret ARN with its suffix, collection endpoint.
|
|
32
|
+
Authorizers given as `!GetAtt X.Arn`, `Ref`, `!Sub '${X.Arn}'` or `$ref` resolve to the local
|
|
33
|
+
function.
|
|
34
|
+
- **Registration diagnostics**: one summary warning listing the resource types LSS does not
|
|
35
|
+
provision (with counts), a warning for an authorizer whose target cannot be found, and one for a
|
|
36
|
+
packaged stage that differs from the packaging command's `--stage` or the previous registration.
|
|
37
|
+
- **`lss reset`** / `POST /_lss/reset` — empties every emulator in place without restarting or
|
|
38
|
+
rebinding the port, persistent and in-memory alike. Loopback-only and gated on the
|
|
39
|
+
`x-lss-confirm-reset: true` header (a CORS preflight the route never answers, so a web page cannot
|
|
40
|
+
trigger it).
|
|
41
|
+
- **S3 bucket configuration**: Put/Get/DeleteBucketEncryption (none → SSE-S3 `AES256`, as AWS since
|
|
42
|
+
2023), tagging, Public Access Block and policy (stored, not enforced). Objects keep
|
|
43
|
+
`Content-Disposition`, `Cache-Control`, `Content-Encoding`, `Content-Language`, `Expires` and their
|
|
44
|
+
SSE headers across PutObject, presigned POST and CopyObject.
|
|
45
|
+
- **SQS**: `ListDeadLetterSourceQueues`, `ListQueues` `MaxResults`/`NextToken` paging, and
|
|
46
|
+
`MaximumMessageSize` enforced on body plus attributes.
|
|
47
|
+
- **DynamoDB TTL sweep**: expired items are deleted by a 30 s background pass (not only on read),
|
|
48
|
+
emitting the stream `REMOVE` with the TTL service identity, so TTL-triggered Lambdas fire on
|
|
49
|
+
their own.
|
|
50
|
+
- **Per-service / per-function runtime overrides**: `serviceRuntime.<svc>.env`, `.execArgv` and
|
|
51
|
+
`.functions.<fn>.env`. `env` is re-read per invocation, so an edit applies without a restart.
|
|
52
|
+
- **`selfEngine.fallbackEndpoints`** (per service, by SigV4 signing name) and the
|
|
53
|
+
`LSS_FALLBACK_ENDPOINT` env var for the catch-all.
|
|
54
|
+
- **API Gateway authorizer cache stats** on `GET /api/apis/authorizer-cache`; dashboard `?region=`
|
|
55
|
+
deep links.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- **Lambda authorizer policies are evaluated** against the request's `methodArn` (`*`/`?`
|
|
60
|
+
wildcards, explicit `Deny` wins). Any `Allow` used to authorize every route. The *policy* is
|
|
61
|
+
cached — one cache per authorizer per API — and re-evaluated per request.
|
|
62
|
+
- **Authorizer errors map like AWS**: a REST authorizer throwing exactly `Unauthorized` is 401;
|
|
63
|
+
any other failure, or a response without `policyDocument.Statement`, is 500. HTTP API authorizer
|
|
64
|
+
failures are 500.
|
|
65
|
+
- **A missing REST route is 403 `Missing Authentication Token`** (HTTP API keeps 404), and `OPTIONS`
|
|
66
|
+
is answered automatically only when CORS is configured.
|
|
67
|
+
- **SQS dead-lettering happens on the receive** that would push `ApproximateReceiveCount` past
|
|
68
|
+
`maxReceiveCount`, not the instant the visibility lapses — the message is still in its source
|
|
69
|
+
queue (and still deletable) in between, as on AWS.
|
|
70
|
+
- **DynamoDB rejects bare reserved words** in expressions (`… reserved keyword: status`), rejects
|
|
71
|
+
`ConsistentRead` on a GSI, caps Query/Scan pages at 1 MB, and evaluates `a <> :v` as true when
|
|
72
|
+
`a` is absent.
|
|
73
|
+
- **`selfEngine.port` defaults to `serverPort`**: setting only `serverPort` / `LSS_DASHBOARD_PORT`
|
|
74
|
+
moves the whole stack instead of also opening 14566.
|
|
75
|
+
- **An explicit `--config` / `LSS_CONFIG` that is missing or unparseable is a hard error** for every
|
|
76
|
+
command, instead of falling back to the default search (which let `lss stop --config ./wrong.json`
|
|
77
|
+
stop another instance).
|
|
78
|
+
- **Lambda workers point at the engine**: `AWS_ENDPOINT_URL`, `test` credentials and the service
|
|
79
|
+
region are set; inherited session tokens, profiles and role credentials are removed. Docs now say
|
|
80
|
+
`AWS_ENDPOINT_URL`, the variable the SDKs actually read.
|
|
81
|
+
- **Unsupported-service errors come back in the caller's protocol** (AWS JSON, Query XML, S3 XML or
|
|
82
|
+
rest-json), name `service.Operation` and link to the coverage table on GitHub.
|
|
83
|
+
- `GET /api/config` exposes runtime `env` maps as key names only (`envKeys`).
|
|
84
|
+
- Dependencies refreshed: `source-map-js`, `handlebars`, `proxy-addr`, `shell-quote` /
|
|
85
|
+
`concurrently`, `brace-expansion`, `adm-zip`, `js-yaml`, `@humanfs/node`,
|
|
86
|
+
`postcss-selector-parser`, and `undici` / `fast-uri` / `js-yaml` in the examples and fixtures.
|
|
87
|
+
- **The repository is English-only.** English was already the language of the source, but not of
|
|
88
|
+
everything around it. Translated: the two PRDs (`docs/PRD_SELF_ENGINE.md`,
|
|
89
|
+
`docs/PRD_API_LAMBDA_EMULATION.md`), the dashboard's UI/UX contract (`src/ui/ui-ux.md`), the
|
|
90
|
+
archived design proposal, the 2026-07-29 handoff — renamed `REVISAO-2026-07-29.md` →
|
|
91
|
+
`REVIEW-2026-07-29.md` — plus the `.vscode/tasks.json` labels/details, the last Portuguese line of
|
|
92
|
+
`TODO.md` and two CHANGELOG entries that quoted CLI output verbatim. What stays non-English is
|
|
93
|
+
what has to: the `pt-BR`/`es` translation catalogues, the language endonyms
|
|
94
|
+
(`Português (BR)`, `Español`), the `sa-east-1 — São Paulo` region label, the `confirmar` token
|
|
95
|
+
`seed:clear` asks the operator to type, and the tests that assert on translated CLI output.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- **S3 subresources no longer fall through**: `PUT /bucket?encryption` was a 409
|
|
100
|
+
`BucketAlreadyOwnedByYou`, `DELETE /bucket?tagging` deleted the bucket and `PUT /key?tagging`
|
|
101
|
+
overwrote the object. Unsupported subresources now answer `NotImplemented` (501).
|
|
102
|
+
- **SQS receipt handles**: deleting with the latest handle after the visibility lapsed answered
|
|
103
|
+
200 and left the message to redeliver; `VisibilityTimeout: 0` left the message invisible until
|
|
104
|
+
the queue timer fired, so an immediate peek missed it roughly half the time.
|
|
105
|
+
- **Artifact mode loads ESM bundles with external packages** — ESM ignores `NODE_PATH`, so LSS now
|
|
106
|
+
links `node_modules` next to the extraction.
|
|
107
|
+
- **Event wiring without an invoke port**: SQS/stream mappings, EventBridge targets and S3
|
|
108
|
+
notifications failed to wire when a service declared no `invokePort`; they are delivered
|
|
109
|
+
in-process.
|
|
110
|
+
- **Function env no longer leaks** between functions sharing a worker.
|
|
111
|
+
- **Hot reload ignores LSS's own state**: a `stateDir` / `dataDir` inside a service caused an
|
|
112
|
+
endless restart loop.
|
|
113
|
+
- **The API Gateway path is decoded once** (`%2525` reached handlers as `%`).
|
|
114
|
+
- **The fallback proxy normalizes a duplicated `Date` header**, which broke the SDK's clock-skew
|
|
115
|
+
correction from the second call on.
|
|
116
|
+
- **Unsigned `GET /?Action=…`** reached the dashboard instead of the engine.
|
|
117
|
+
|
|
118
|
+
- **The dashboard's detected language now reaches `<html lang>` at boot.** The locale resolution
|
|
119
|
+
(stored choice → `navigator.languages` → English) already ran on load, but only an explicit switch
|
|
120
|
+
in the ⋮ menu wrote the result to the document. A browser set to pt-BR therefore got a fully
|
|
121
|
+
translated dashboard that still declared `lang="en"` — the tag `index.html` can only ship
|
|
122
|
+
statically — so screen readers announced Portuguese with English pronunciation and the browser's
|
|
123
|
+
own translate prompt offered to translate a page already in the reader's language. Resolution and
|
|
124
|
+
the manual switch now share one `applyDocumentLang()`, and `tests/unit/ui/i18n-messages.test.ts`
|
|
125
|
+
pins the whole chain (browser languages, stored override, storage disabled, boot, switch) against
|
|
126
|
+
a stubbed browser.
|
|
127
|
+
|
|
8
128
|
## [1.0.0] - 2026-07-31
|
|
9
129
|
|
|
10
130
|
**LSS runs on one engine.** The LocalStack backend is gone — not deprecated, removed. The self
|
|
@@ -503,7 +623,7 @@ Corrections from integrating LSS 0.5.0 in a real monorepo using osls 4, serverle
|
|
|
503
623
|
- **Native TypeScript source loading when available**: in `source` mode, the runtime worker now tries Node's native TypeScript type stripping (`process.features.typescript`) before requiring `esbuild-register`, `tsx`, or `ts-node`.
|
|
504
624
|
|
|
505
625
|
### Fixed
|
|
506
|
-
- **Artifact resolution for osls/serverless package output**: `package.artifact` values such as `
|
|
626
|
+
- **Artifact resolution for osls/serverless package output**: `package.artifact` values such as `acme-identity.zip` are now resolved relative to `.serverless/` as well as the service root, and nonexistent declared candidates no longer suppress the `.serverless/*.zip` scan fallback.
|
|
507
627
|
- **Stale LocalStack proxy `INVOKE_URL`**: re-registering a service now updates an existing proxy Lambda when the expected invoke URL changed (port renumbering or `invokeHost` changes).
|
|
508
628
|
- **Service identity collisions**: service registration now uses the `service:` name from `serverless-state.json` instead of the directory basename, with migration for same-root legacy cache entries.
|
|
509
629
|
- **Event source mapping fidelity**: CloudFormation `StartingPosition`, `MaximumRetryAttempts`, `FunctionResponseTypes`, `MaximumBatchingWindowInSeconds`, `FilterCriteria`, and `DestinationConfig.OnFailure` are parsed/applied where supported; duplicate checks compare the resolved live ARN instead of unresolved CFN refs.
|
|
@@ -581,13 +701,13 @@ Testability features for downstream e2e suites: an isolated test instance and a
|
|
|
581
701
|
## [0.1.2] - 2026-05-26
|
|
582
702
|
|
|
583
703
|
### Added
|
|
584
|
-
- **`npx lss seed:clear` now requires interactive confirmation**: before issuing any `DeleteRequest`, the CLI lists the exact LocalStack tables it would wipe, prints the LocalStack URL, makes the "this never touches AWS" guarantee explicit, and waits for the user to type `confirmar` (case-sensitive, exact match after trim). Anything else cancels with `🚫
|
|
704
|
+
- **`npx lss seed:clear` now requires interactive confirmation**: before issuing any `DeleteRequest`, the CLI lists the exact LocalStack tables it would wipe, prints the LocalStack URL, makes the "this never touches AWS" guarantee explicit, and waits for the user to type `confirmar` (case-sensitive, exact match after trim). Anything else cancels with `🚫 Cancelled — nothing was changed.` and no clear call is made. `--yes` / `-y` skips the prompt for CI use.
|
|
585
705
|
- **Defensive endpoint guard in `SeedManager`**: `clearTable` and `clearAllSeeded` now refuse to run unless the resolved DynamoDB endpoint hostname is on a hardcoded local allowlist (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`, `host.docker.internal`, `localstack`, `lss-localstack`, `lss-localstack-<port>`, `*.localhost`). If a future refactor ever pointed `LocalStackManager.getConfig()` at a real AWS endpoint, the guard would throw `Refusing destructive operation: endpoint "..." is not a recognized local LocalStack host. seed:clear may ONLY run against LocalStack — never against AWS.` before any AWS SDK call. Architecture already pinned writes to LocalStack via fake credentials; this is belt-and-braces.
|
|
586
706
|
- **Seed/clear mismatch diagnostic**: `GET /api/seeds` now also returns `liveTables: string[]` (every DynamoDB table currently in LocalStack). When `npx lss seed` skips tables or `npx lss seed:clear` finds no live targets, the CLI prints a two-column diagnostic — seed files inspected on one side, live LocalStack tables on the other — with the hint "Os nomes dos arquivos de seed precisam bater EXATAMENTE com o `TableName` no CloudFormation." This makes seed-name/table-name typos obvious instead of "nothing happened, why?". When the live-tables list is empty, the CLI falls back to the older "run `npx lss start` + `npx serverless deploy` first" hint.
|
|
587
707
|
- **Test infrastructure**: new `tests/unit/seed-manager-guard.test.ts` (27 tests covering the endpoint allowlist matrix, IPv6 bracket handling, malformed URLs, and that `clearTable`/`clearAllSeeded` invoke the guard) and `tests/unit/cli-seed.test.ts` (21 tests spawning `bin/cli.js` against an in-process HTTP stub of the orchestrator — covers confirmation flow, `--yes`/`-y` bypass, "no live tables" diagnostic, name-mismatch diagnostic, and `formatError` robustness against 500-with-empty-body / 500-with-non-JSON / `{error: ""}` responses).
|
|
588
708
|
|
|
589
709
|
### Changed
|
|
590
|
-
- **`bin/cli.js` error handling never leaves the user with an empty message**: new `formatError(e)` and `buildHttpError(res, data)` helpers walk a fallback chain (`e.message` → `e.code` → `e.name` → body snippet → HTTP status text → "
|
|
710
|
+
- **`bin/cli.js` error handling never leaves the user with an empty message**: new `formatError(e)` and `buildHttpError(res, data)` helpers walk a fallback chain (`e.message` → `e.code` → `e.name` → body snippet → HTTP status text → "unknown error (no details)") so a response 500 with no body, a socket reset during orchestrator startup, or an `Error` with empty `.message` all produce a useful CLI line instead of `❌ Could not list the tables before clearing:` with a blank suffix. Applied to every `console.error` path in `seed`, `seed:clear`, and the underlying `getJson`/`postJson` helpers.
|
|
591
711
|
- **`npx lss seed` hint when tables are missing in LocalStack**: previously printed a generic "tabelas foram puladas" footer. Now it fetches `liveTables` and shows the same mismatch diagnostic as `seed:clear`, so the user sees whether the cause is "I haven't deployed yet" (no live tables) or "my seed file name is wrong" (live tables exist but don't match).
|
|
592
712
|
- **`firstPositional()` arg parser in the CLI**: commands that accept an optional table name (`seed`, `seed:clear`) now skip args starting with `-`. Without this, `npx lss seed:clear --yes` was interpreting `--yes` as the table name.
|
|
593
713
|
- **`SeedManager.assertLocalEndpoint` normalizes IPv6 brackets** so URLs like `http://[::1]:4566` (which `new URL().hostname` reports as `[::1]`) match the `::1` entry in the allowlist instead of being rejected as non-local.
|
|
@@ -958,6 +1078,8 @@ Not applicable (initial release)
|
|
|
958
1078
|
|
|
959
1079
|
## [Unreleased]
|
|
960
1080
|
|
|
1081
|
+
## [1.1.0] - 2026-10-18
|
|
1082
|
+
|
|
961
1083
|
### Planned Features
|
|
962
1084
|
- S3 bucket provisioning
|
|
963
1085
|
- EventBridge integration
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ flowchart LR
|
|
|
23
23
|
|
|
24
24
|
Every feature LSS ships, grouped by area. Each one expands into **how it works** (the real
|
|
25
25
|
mechanism and defaults), **where it lives** (the files that own it) and **why it exists** (the
|
|
26
|
-
problem it solves).
|
|
26
|
+
problem it solves). 92 features in nine areas — start with [Quick Start](#quick-start) if you just
|
|
27
27
|
want it running.
|
|
28
28
|
|
|
29
29
|
> **⚠️ Keep this inventory alive.** A new capability MUST be added here **and** to
|
|
@@ -56,7 +56,9 @@ want it running.
|
|
|
56
56
|
<details>
|
|
57
57
|
<summary><b><code>lss register [path...]</code></b> — attach a service to the running stack without any Serverless plugin</summary>
|
|
58
58
|
|
|
59
|
-
**How it works.** It requires a running orchestrator (PID file present, otherwise it prints the start hint and exits 1), then resolves each positional argument to an absolute path — defaulting to `.` when none is given — and skips (counting as failed) any directory that does not exist. For each path it `POST`s `{ servicePath }` to `/api/services/register` on the server port; the orchestrator does everything else: packaging on demand via `autoPackage`, then reading name, region and `custom.lss` ports out of the packaged `serverless-state.json`. Each success prints resource/function/route counts plus any warnings the server returned — including everything provisioning could **not** do (a resource that failed, a rule waiting for a bus, HTTP routes with no `apiPort`), which used to reach the daemon log only; if any target failed, the process exits 1.
|
|
59
|
+
**How it works.** It requires a running orchestrator (PID file present, otherwise it prints the start hint and exits 1), then resolves each positional argument to an absolute path — defaulting to `.` when none is given — and skips (counting as failed) any directory that does not exist. For each path it `POST`s `{ servicePath, repackage }` to `/api/services/register` on the server port; the orchestrator does everything else: packaging on demand via `autoPackage`, then reading name, region and `custom.lss` ports out of the packaged `serverless-state.json`. Each success prints resource/function/route counts plus any warnings the server returned — including everything provisioning could **not** do (a resource that failed, a rule waiting for a bus, HTTP routes with no `apiPort`), which used to reach the daemon log only; if any target failed, the process exits 1.
|
|
60
|
+
|
|
61
|
+
**`--repackage`, and the warning that made it necessary.** `autoPackage` packages only when the template is **missing**, so registering an already-packaged service re-reads the artifacts on disk — which may be many commits old. The command answers `✓` in a fraction of a second either way, and since registering is the natural gesture after editing code, that `✓` reads as "my code is loaded" when it can mean "yesterday's build is loaded". Two changes: registration now compares the newest artifact in `.serverless/` against the service's sources (skipping `node_modules`, `dist`, `build`, `coverage` and dot-directories) and emits a warning naming the file that moved, both timestamps and the service's own package command; and `--repackage` runs that command before reading, regardless of `autoPackage` — an explicit operator gesture, not an automatic one. The warning never blocks the registration: what was packaged is still registered, it just stops being silent about its age.
|
|
60
62
|
|
|
61
63
|
**Where it lives.** `bin/cli.js` — `registerServices()`, `postJson()`, `allPositionals()`. `src/server/routes/services.ts` — `POST /register`, which validates the path and delegates to the registrar.
|
|
62
64
|
|
|
@@ -108,10 +110,21 @@ want it running.
|
|
|
108
110
|
|
|
109
111
|
</details>
|
|
110
112
|
|
|
113
|
+
<details>
|
|
114
|
+
<summary><b><code>lss reset</code></b> — empty every emulator in place, without restarting the stack</summary>
|
|
115
|
+
|
|
116
|
+
**How it works.** After the same `confirmar` prompt as `seed:clear` (or `--yes`), it posts to the engine's `POST /_lss/reset`, which stops event delivery, flushes the store, deletes the data dir's contents in persistent mode (refusing a directory that looks like a project or home dir) and rebuilds every emulator over an empty store on the same listener — tables, queues, buckets, topics, buses and rules, secrets, parameters, OpenSearch collections, Lambda metadata and mappings, all gone; requests that arrive meanwhile wait for it. The route only answers loopback clients and requires the `x-lss-confirm-reset: true` header, which forces a CORS preflight it never answers, so a web page open in your browser cannot trigger it. Registered services stay registered: `lss register` puts their resources back.
|
|
117
|
+
|
|
118
|
+
**Where it lives.** `bin/cli.js` — the `reset` command. `src/server/engine/http/reset.ts` — the guarded route. `src/server/engine/backends/self-backend.ts` — `reset()`, `wipeDataDir()`.
|
|
119
|
+
|
|
120
|
+
**Why it exists.** A clean slate between test runs or demos used to mean stopping the stack, finding the data dir and deleting it by hand — or deleting resources one by one through each service's API.
|
|
121
|
+
|
|
122
|
+
</details>
|
|
123
|
+
|
|
111
124
|
<details>
|
|
112
125
|
<summary><b>Per-instance config and state isolation</b> — <code>--config</code>, <code>stateDir</code> and env overrides let several stacks coexist on one machine</summary>
|
|
113
126
|
|
|
114
|
-
**How it works.** Config resolution is: `--config <path>` (or `LSS_CONFIG`), resolved absolute, then `./lss.config.json`, `./.lssrc`, `$HOME/lss.config.json`, `$HOME/.lssrc` — a missing or unparseable explicit file
|
|
127
|
+
**How it works.** Config resolution is: `--config <path>` (or `LSS_CONFIG`), resolved absolute, then `./lss.config.json`, `./.lssrc`, `$HOME/lss.config.json`, `$HOME/.lssrc` — a missing or unparseable explicit file is a hard error (exit 1) for every command: falling back to the search would let `lss stop --config ./wrong.json` stop whichever instance the default config addresses. Env overrides mirror the server's exactly: `LSS_DASHBOARD_PORT` or `PORT` for `serverPort` (the engine follows it unless `selfEngine.port` is set to something else), `LSS_ENGINE_PORT` for the engine, `LSS_DYNAMO_PROXY_PORT`, and `LSS_ENABLE_DYNAMO_PROXY` (`true` or `1`); a non-numeric port is ignored rather than becoming `NaN`. PID/log paths follow from that: with a `stateDir` they are `<stateDir>/orchestrator.pid` and `.log`; otherwise `lss-orchestrator.pid`/`.log` in the OS temp dir on the default port 14566, or `lss-orchestrator-<port>.*` on any other. When `--config` is used, the file is also handed to the server as `LSS_CONFIG_PATH` so both config loaders read the same file.
|
|
115
128
|
|
|
116
129
|
**Where it lives.** `bin/cli.js` — `loadConfig()`, `getConfig()`, `envPort()`, `runtimePaths()`, `EXPLICIT_CONFIG`. `src/server/services/config-manager.ts` — the server-side loader that honours `LSS_CONFIG_PATH` and the same env vars.
|
|
117
130
|
|
|
@@ -179,7 +192,7 @@ want it running.
|
|
|
179
192
|
<details>
|
|
180
193
|
<summary><b>On-demand packaging (<code>autoPackage</code>)</b> — register a service that was never packaged, and LSS packages it for you</summary>
|
|
181
194
|
|
|
182
|
-
**How it works.** When the CloudFormation template is missing and `autoPackage` is on (default **false**; `LSS_AUTO_PACKAGE` also sets it), the registrar runs the effective package command with the service directory as cwd — default `npx serverless package`, global `packageArgs` then any per-service `packageArgs` appended as discrete argv (no shell re-parsing), `packageEnv` merged over `process.env`, timeout `300000` ms with `SIGTERM` then `SIGKILL` 2 s later — and re-reads the template. A non-zero exit or a timeout raises `ServerlessPackageError`; the registrar logs the full stdout/stderr and answers 500 pointing at `/tmp/lss-orchestrator.log`. With `autoPackage` off, a missing template is a 400 telling you to run `serverless package`. `POST /api/services/package` runs the same effective command on demand and returns `exitCode`, `durationMs` and the last 8000 characters of output (422 on failure). The source watcher's full reload re-packages the same way before re-registering.
|
|
195
|
+
**How it works.** When the CloudFormation template is missing and `autoPackage` is on (default **false**; `LSS_AUTO_PACKAGE` also sets it), the registrar runs the effective package command with the service directory as cwd — default `npx serverless package`, global `packageArgs` then any per-service `packageArgs` appended as discrete argv (no shell re-parsing), `packageEnv` merged over `process.env`, timeout `300000` ms with `SIGTERM` then `SIGKILL` 2 s later — and re-reads the template. A non-zero exit or a timeout raises `ServerlessPackageError`; the registrar logs the full stdout/stderr and answers 500 pointing at `/tmp/lss-orchestrator.log`. With `autoPackage` off, a missing template is a 400 telling you to run `serverless package`. `POST /api/services/package` runs the same effective command on demand and returns `exitCode`, `durationMs` and the last 8000 characters of output (422 on failure). The source watcher's full reload re-packages the same way before re-registering. Because the trigger is a **missing** template, `autoPackage` never refreshes a service that was packaged once — that is what `lss register --repackage` (`{ repackage: true }` on the API) is for, and what the stale-artifact warning reports when neither ran.
|
|
183
196
|
|
|
184
197
|
**What may be run.** `packageCommand` is settable through `PUT /api/config`, which has no authentication, and it becomes `spawn(firstToken, restTokens)` with `packageArgs` appended — so validating it as "any non-empty string" made the `/start` and `/install` allowlists pointless, since two calls reached the same `spawn()`. Checking only the **first token** does not close it either, and that version of the fence was walked straight through: every package manager is itself an interpreter one subcommand in, and `spawn('npm', ['exec','-c','<shell string>'])` runs that string with no `shell: true` anywhere — as do `npx -c`, `yarn dlx`, `pnpm dlx`, `yarn node -e` and `npm i <pkg>` (install scripts). So what a command written through that endpoint must match — global or per-service — is a **grammar over the whole argv**, tokenized exactly the way the packager tokenizes it before `spawn()`:
|
|
185
198
|
|
|
@@ -235,7 +248,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
235
248
|
<details>
|
|
236
249
|
<summary><b>Packaged template → typed resource graph</b> — LSS reads the same CloudFormation you deploy and turns it into the resources it will create locally</summary>
|
|
237
250
|
|
|
238
|
-
**How it works.** On `lss register` (or `POST /api/services/register`) the registrar reads `<service>/.serverless/cloudformation-template-update-stack.json` — running `serverless package` first when the file is missing and `autoPackage` is on — and `CloudFormationParser.parse()` walks `Resources`, switching on `Type`. Supported types: `AWS::Lambda::Function`, `AWS::DynamoDB::Table`, `AWS::SQS::Queue`, `AWS::SNS::Topic`, `AWS::S3::Bucket`, `AWS::SecretsManager::Secret`, `AWS::Lambda::EventSourceMapping`, `AWS::Events::EventBus`, `AWS::Events::Rule`, `AWS::OpenSearchServerless::Collection`, `AWS::ApiGatewayV2::Api` / `::Integration` / `::Route` / `::Authorizer`, and `AWS::Lambda::Permission`. Unknown types are
|
|
251
|
+
**How it works.** On `lss register` (or `POST /api/services/register`) the registrar reads `<service>/.serverless/cloudformation-template-update-stack.json` — running `serverless package` first when the file is missing and `autoPackage` is on — and `CloudFormationParser.parse()` walks `Resources`, switching on `Type`. Supported types: `AWS::Lambda::Function`, `AWS::DynamoDB::Table`, `AWS::SQS::Queue`, `AWS::SNS::Topic`, `AWS::S3::Bucket`, `AWS::SecretsManager::Secret`, `AWS::SSM::Parameter`, `AWS::Lambda::EventSourceMapping`, `AWS::Events::EventBus`, `AWS::Events::Rule`, `AWS::OpenSearchServerless::Collection`, `AWS::ApiGatewayV2::Api` / `::Integration` / `::Route` / `::Authorizer`, and `AWS::Lambda::Permission`. Unknown types are not provisioned, and registration says so in **one** summary warning sorted by count (`Skipped 110 of 151 resource(s) whose type LSS does not provision: AWS::IAM::Role ×20, …`); the Serverless Framework's `ServerlessDeploymentBucket` is dropped by logical id without counting; `AWS::Events::Archive` and `AWS::OpenSearchServerless::SecurityPolicy` / `::AccessPolicy` / `::VpcEndpoint` push an explicit warning ("not provisioned locally" / "no-op locally") into the `warnings[]` array returned to the caller and logged as `⚠️ [service] …`. A SHA-256 of the whole template is stored as the service's `templateHash`.
|
|
239
252
|
|
|
240
253
|
**Where it lives.** `src/server/services/cloudformation-parser.ts` — the type switch, one parser per resource type, and `calculateHash`. `src/server/services/service-registrar.ts` — reads the template (with auto-package fallback), collects warnings, persists metadata and calls the provisioner.
|
|
241
254
|
|
|
@@ -243,6 +256,17 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
243
256
|
|
|
244
257
|
</details>
|
|
245
258
|
|
|
259
|
+
<details>
|
|
260
|
+
<summary><b>CloudFormation intrinsics and physical values</b> — names built with <code>!Sub</code>, parameters and conditions resolve, and function env gets real ARNs and URLs</summary>
|
|
261
|
+
|
|
262
|
+
**How it works.** Before any resource is read, a static pass resolves everything that is knowable without deploying: `Parameters` (an override, else the `Default`; list types split on commas; SSM-typed parameters are refused, since their default is a path, not a value), the pseudo parameters (`AWS::Region`, `AccountId`, `StackName` — `provider.stackName` or `<service>-<stage>` — `StackId`, `Partition`, `URLSuffix`, `NotificationARNs`, `NoValue`, which removes the property), `Mappings`, `Conditions` (`Fn::Equals`/`And`/`Or`/`Not`/`Condition`, memoized and cycle-safe) and the string/list functions (`Fn::Sub` in both forms with `${!x}` escapes, `Join`, `Select`, `Split`, `If`, `FindInMap`, `GetAZs`, `Base64`, `ImportValue` from the stack export map). A resource whose `Condition` is false is not created and named in one warning; an undecidable condition keeps it. A `Ref`/`GetAtt` to a *resource* is left in place until its physical value exists — after provisioning, the same pass reruns over every function's `Environment.Variables` (from the template and from `serverless-state.json`) with AWS's documented return values: a table `Ref` is its name and `StreamArn` the live stream ARN, a queue `Ref` its URL and `Arn`/`QueueName` the obvious, a secret `Ref` its ARN with the random suffix, a Lambda `Ref` its name, plus SNS, S3, EventBridge, OpenSearch Serverless and SSM parameters; values only the engine knows are read back from it. Anything still unresolvable keeps a recognizable fallback and is reported once in `warnings[]` — an object is never JSON-stringified into an env var silently. Authorizers given as `arn: !GetAtt XLambdaFunction.Arn` (also `Ref`, a lone `!Sub '${X.Arn}'` and `serverless-state.json` `$ref` pointers) resolve to the local function, and one that cannot be resolved is a registration warning instead of a 500 on every request. Registration also warns when the packaged stage differs from the packaging command's `--stage` or from the previous registration of the same service.
|
|
263
|
+
|
|
264
|
+
**Where it lives.** `src/server/services/cfn-intrinsics.ts` — the pure resolver. `src/server/services/cfn-physical-values.ts` — per-type `Ref`/`GetAtt` values. `src/server/services/cloudformation-parser.ts` — `parseTemplate()`, the static pass and the skipped-types summary. `src/server/services/service-registrar.ts` — environment resolution after provisioning, the stage check. `src/server/services/resource-provisioner.ts` — `lookupRuntimeAttributes()`.
|
|
265
|
+
|
|
266
|
+
**Why it exists.** Real templates name their resources with `!Sub '${AWS::StackName}-orders'` and hand them to functions as `!Ref OrdersTable`; without resolution the table is never created and every handler reads `TABLE_NAME=OrdersTable`, so the stack only ran after a hand-written adapter rewrote the package.
|
|
267
|
+
|
|
268
|
+
</details>
|
|
269
|
+
|
|
246
270
|
<details>
|
|
247
271
|
<summary><b>Idempotent, non-fatal provisioning passes</b> — registering the same service ten times converges instead of failing</summary>
|
|
248
272
|
|
|
@@ -380,7 +404,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
380
404
|
<details>
|
|
381
405
|
<summary><b>In-process Lambda runtime</b> — your handlers run in a forked Node worker per service, with real Lambda context, timeouts and warm starts</summary>
|
|
382
406
|
|
|
383
|
-
**How it works.** Each registered service gets at most one forked worker process (`stdio: ignore/pipe/pipe/ipc`, `cwd` = service root, `NODE_PATH` pointed at the service's own `node_modules`). An invoke is an IPC message carrying handler, event, env, region, memory and timeout; the worker resolves the handler file (`.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, `.mts`, or `index.*` in a directory), caches the loaded module between invocations for warm-start behaviour, and runs it with an AWS-shaped `context` (`awsRequestId`, `invokedFunctionArn`, `logGroupName: /aws/lambda/<fullName>`, `getRemainingTimeInMillis()`) plus `callback`-style support when the handler takes 3 arguments. Per-function `timeout` (default 6 s) and `memorySize` (default 1024 MB) come from the packaged state; the worker rejects with a `TimeoutError` at the deadline and the manager adds a 5 s IPC grace on top so a wedged worker still answers. TypeScript sources load through native type stripping when `process.features.typescript` is on, otherwise via `esbuild-register`, `tsx/cjs` or `ts-node/register/transpile-only` resolved from the service's `node_modules` first. A crashed worker is restarted up to 3 times per 30 s window before the runtime is parked in `error`; the last 50 invocations per service are kept with their logs, duration and HTTP status.
|
|
407
|
+
**How it works.** Each registered service gets at most one forked worker process (`stdio: ignore/pipe/pipe/ipc`, `cwd` = service root, `NODE_PATH` pointed at the service's own `node_modules`). An invoke is an IPC message carrying handler, event, env, region, memory and timeout; the worker resolves the handler file (`.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, `.mts`, or `index.*` in a directory), caches the loaded module between invocations for warm-start behaviour, and runs it with an AWS-shaped `context` (`awsRequestId`, `invokedFunctionArn`, `logGroupName: /aws/lambda/<fullName>`, `getRemainingTimeInMillis()`) plus `callback`-style support when the handler takes 3 arguments. Per-function `timeout` (default 6 s) and `memorySize` (default 1024 MB) come from the packaged state; the worker rejects with a `TimeoutError` at the deadline and the manager adds a 5 s IPC grace on top so a wedged worker still answers. TypeScript sources load through native type stripping when `process.features.typescript` is on, otherwise via `esbuild-register`, `tsx/cjs` or `ts-node/register/transpile-only` resolved from the service's `node_modules` first. A crashed worker is restarted up to 3 times per 30 s window before the runtime is parked in `error`; the last 50 invocations per service are kept with their logs, duration and HTTP status. Every worker is pointed at the engine and away from real AWS: `AWS_ENDPOINT_URL` is the engine endpoint, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` are `test`, `AWS_REGION`/`AWS_DEFAULT_REGION` are the service's region, and inherited session tokens, profiles, role / web-identity / container credentials and `AWS_ENDPOINT_URL_<SERVICE>` are removed — a template or override env that sets one explicitly still wins. Each invocation starts from the worker's fork env, so one function's variables never leak into the next function sharing the worker. `serviceRuntime.<svc>.env`, `.execArgv` and `.functions.<fn>.env` layer local overrides on top (function > service > template); `env` is re-read per invocation, so a dashboard edit applies on the next call. A declared `nodejsNN.x` that differs from the worker's Node major is warned about.
|
|
384
408
|
|
|
385
409
|
**Where it lives.** `src/server/services/lambda-runtime-manager.ts` — fork/restart supervision, invoke brokering, invocation history. `src/server/runtime/runtime-worker.ts` — handler resolution, TS loaders, context/timeout, IPC protocol. `src/server/services/function-registry.ts` — global name/ARN → function resolution shared by every invoke path. `src/server/routes/lambdas.ts` — `GET /api/lambdas`, `POST /api/lambdas/:name/invoke`, `GET /api/lambdas/:name/logs`.
|
|
386
410
|
|
|
@@ -391,7 +415,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
391
415
|
<details>
|
|
392
416
|
<summary><b>API Gateway emulation</b> — each service's HTTP routes are served on its own port with faithful v1/v2 payloads</summary>
|
|
393
417
|
|
|
394
|
-
**How it works.** For every service with routes and an `apiPort` (the examples use 3631–3634, declared as `custom.lss.apiPort`), the orchestrator binds an HTTP listener *inside its own process*. A request is matched against the service's routes by specificity — literal segment 100, `{param}` 10, `{proxy+}` 1, the HTTP API `$default` catch-all 0, with an exact method beating `ANY` by one point. The matched route builds either a REST payload format 1.0 event (`http`) or an HTTP API 2.0 event (`httpApi`, including split `cookies` and `routeKey`), invokes the target function, and maps the result back: v1 demands a well-formed `{statusCode,…}` and answers 502 otherwise, v2 accepts a bare return value and infers `200 application/json`. Binary bodies are base64-encoded unless the content type is textual (`text/*`, `application/json|xml|javascript|x-www-form-urlencoded`, `*+json`/`*+xml`). `OPTIONS` preflight is answered by the gateway itself (204, `access-control-max-age: 300`, allow-headers echoing the request or `Content-Type,Authorization,X-Api-Key`) when
|
|
418
|
+
**How it works.** For every service with routes and an `apiPort` (the examples use 3631–3634, declared as `custom.lss.apiPort`), the orchestrator binds an HTTP listener *inside its own process*. A request is matched against the service's routes by specificity — literal segment 100, `{param}` 10, `{proxy+}` 1, the HTTP API `$default` catch-all 0, with an exact method beating `ANY` by one point. The matched route builds either a REST payload format 1.0 event (`http`) or an HTTP API 2.0 event (`httpApi`, including split `cookies` and `routeKey`), invokes the target function, and maps the result back: v1 demands a well-formed `{statusCode,…}` and answers 502 otherwise, v2 accepts a bare return value and infers `200 application/json`. Binary bodies are base64-encoded unless the content type is textual (`text/*`, `application/json|xml|javascript|x-www-form-urlencoded`, `*+json`/`*+xml`). `OPTIONS` preflight is answered by the gateway itself (204, `access-control-max-age: 300`, allow-headers echoing the request or `Content-Type,Authorization,X-Api-Key`) only when CORS is configured — API-wide for HTTP APIs, per resource for REST `cors: true` — otherwise it goes to an `OPTIONS`/`ANY` route or is a miss; a handler failure returns 500 for HTTP APIs and 502 for REST. A missing route answers like the API type: REST 403 `{"message":"Missing Authentication Token"}`, HTTP API 404 `{"message":"Not Found"}`. Each request gets one id, shared by the authorizer and integration events' `requestContext.requestId` and the `x-amzn-RequestId` (REST) / `apigw-requestid` (HTTP API) response header. The path is percent-decoded exactly once, so `%2F` stays inside a path parameter and `%2525` arrives as `%25`.
|
|
395
419
|
|
|
396
420
|
**Where it lives.** `src/server/services/gateway-manager.ts` — listener lifecycle, request/response plumbing, CORS. `src/server/services/api-gateway-events.ts` — pure route matching, v1/v2 event builders, response mapping. `src/server/services/serverless-state-parser.ts` — routes from `functions[].events`. `src/server/services/raw-api-assembler.ts` — routes from raw `AWS::ApiGatewayV2::*` resources, de-duplicated against state routes by `METHOD /path` (state wins).
|
|
397
421
|
|
|
@@ -413,7 +437,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
413
437
|
<details>
|
|
414
438
|
<summary><b>Execution modes: artifact, source or auto</b> — run the packaged bundle or the raw source tree, per service</summary>
|
|
415
439
|
|
|
416
|
-
**How it works.** `lambdaRuntime.execution` (and per-service `serviceRuntime.<name>.execution`) picks how handler code is located. `artifact` resolves the zips declared in `serverless-state.json` — trying absolute, `.serverless/`-relative and root-relative interpretations, then falling back to scanning `.serverless/*.zip` — and extracts them into a content-addressed directory `~/.lss/orchestrator/runtime/<project>/<service>/<sha256-12 of path:size:mtime>`, so an unchanged package is never re-extracted and stale extractions are pruned. `source` loads handlers straight from the service root. `auto` (the default) picks artifact when a zip exists and source otherwise. Resolution happens at registration even in lazy mode, so a missing package fails loudly right away (`execution mode is "artifact" but no packaged zip was found under <root>/.serverless`) instead of at first request; the resolved mode is reported per service as `resolvedMode`.
|
|
440
|
+
**How it works.** `lambdaRuntime.execution` (and per-service `serviceRuntime.<name>.execution`) picks how handler code is located. `artifact` resolves the zips declared in `serverless-state.json` — trying absolute, `.serverless/`-relative and root-relative interpretations, then falling back to scanning `.serverless/*.zip` — and extracts them into a content-addressed directory `~/.lss/orchestrator/runtime/<project>/<service>/<sha256-12 of path:size:mtime>`, so an unchanged package is never re-extracted and stale extractions are pruned. `source` loads handlers straight from the service root. `auto` (the default) picks artifact when a zip exists and source otherwise — any zip under `.serverless/`, a stale one included, so pin `execution: "source"` for a service you are editing. An ESM bundle that keeps a dependency external resolves it: ESM ignores `NODE_PATH`, so LSS links `node_modules` next to the extraction (the service's own, and a monorepo's hoisted root one level above). Resolution happens at registration even in lazy mode, so a missing package fails loudly right away (`execution mode is "artifact" but no packaged zip was found under <root>/.serverless`) instead of at first request; the resolved mode is reported per service as `resolvedMode`.
|
|
417
441
|
|
|
418
442
|
**Where it lives.** `src/server/services/artifact-resolver.ts` — the three interpretations of `package.artifact` plus the `.serverless` scan. `src/server/services/lambda-runtime-manager.ts` — mode resolution and content-addressed extraction. `src/server/services/config-manager.ts` — `lambdaRuntime.execution` / `serviceRuntime` merge.
|
|
419
443
|
|
|
@@ -435,7 +459,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
435
459
|
<details>
|
|
436
460
|
<summary><b>Hot reload</b> — save a handler and the next request runs the new code</summary>
|
|
437
461
|
|
|
438
|
-
**How it works.** Each service root is watched non-recursively (that's where `serverless.yml` and `package.json` live) plus one recursive watch per non-ignored child directory — `node_modules`, `.serverless`, `.esbuild`, `.build`, `.git`, `dist` and `.lss` are never watched, not merely filtered, which is what keeps 40 services from asking the kernel for ~70k inotify watches. Changes are debounced 500 ms and classified: a touched `serverless.yml`/`.yaml`/`.json` or `package.json` at the service root triggers a **full** reload (re-package when `autoPackage` is on, then re-register with the ports and region already on record), anything else triggers a **runtime** reload — a worker restart, which is all that's needed to flush its module cache. Watching is on by default in `source` mode and off in `artifact` mode (repackaging every save is expensive); `lambdaRuntime.watch` / `serviceRuntime.<name>.watch` overrides that. Last reload time, kind and error are exposed by `GET /api/services/:name/runtime`.
|
|
462
|
+
**How it works.** Each service root is watched non-recursively (that's where `serverless.yml` and `package.json` live) plus one recursive watch per non-ignored child directory — `node_modules`, `.serverless`, `.esbuild`, `.build`, `.git`, `dist` and `.lss` are never watched, not merely filtered, which is what keeps 40 services from asking the kernel for ~70k inotify watches. Changes are debounced 500 ms and classified: a touched `serverless.yml`/`.yaml`/`.json` or `package.json` at the service root triggers a **full** reload (re-package when `autoPackage` is on, then re-register with the ports and region already on record), anything else triggers a **runtime** reload — a worker restart, which is all that's needed to flush its module cache. LSS's own state is never a trigger: a `stateDir` or `selfEngine.dataDir` placed inside a service is skipped, so the engine writing its WAL cannot restart the worker in a loop. Watching is on by default in `source` mode and off in `artifact` mode (repackaging every save is expensive); `lambdaRuntime.watch` / `serviceRuntime.<name>.watch` overrides that. Last reload time, kind and error are exposed by `GET /api/services/:name/runtime`.
|
|
439
463
|
|
|
440
464
|
**Where it lives.** `src/server/services/source-watcher.ts` — the watch topology, debounce and change classification. `src/server/services/service-registrar.ts` — `activate()` (watch defaults) and `reregister()` (the full-reload path). `src/server/index.ts` — wires `onRuntimeReload` → `restartRuntime`, `onFullReload` → `reregister`.
|
|
441
465
|
|
|
@@ -446,7 +470,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
446
470
|
<details>
|
|
447
471
|
<summary><b>Lambda authorizers</b> — REST TOKEN/REQUEST and HTTP API authorizers, including ones owned by another service</summary>
|
|
448
472
|
|
|
449
|
-
**How it works.** When a matched route names an authorizer, the gateway extracts every configured identity source — `$request.header.x` / `$request.querystring.x`, `method.request.header.x` / `method.request.querystring.x`, or a bare header name — and answers **401** without invoking anything if any of them is missing, exactly as API Gateway does. The event shape follows the config: `TOKEN` for REST token authorizers, `REQUEST` for REST/payload-1.0, and the 2.0 shape (with `routeArn`, `identitySource`) otherwise. The target is resolved by function name or ARN through the global registry, so an authorizer living in another registered service works regardless of registration order. Results are interpreted either as simple responses (`isAuthorized`) or as an IAM policy
|
|
473
|
+
**How it works.** When a matched route names an authorizer, the gateway extracts every configured identity source — `$request.header.x` / `$request.querystring.x`, `method.request.header.x` / `method.request.querystring.x`, or a bare header name — and answers **401** without invoking anything if any of them is missing, exactly as API Gateway does. The event shape follows the config: `TOKEN` for REST token authorizers, `REQUEST` for REST/payload-1.0, and the 2.0 shape (with `routeArn`, `identitySource`) otherwise. The target is resolved by function name or ARN through the global registry, so an authorizer living in another registered service works regardless of registration order. Results are interpreted either as simple responses (`isAuthorized`) or as an IAM policy evaluated against the request's `methodArn` (`arn:aws:execute-api:<region>:<account>:lss-local/<stage>/<METHOD>/<path>`): a statement applies when its Action matches `execute-api:Invoke` and a Resource matches with `*`/`?` wildcards, an applying `Deny` wins, and no applying `Allow` is **403**. A REST authorizer that throws exactly `Unauthorized` is **401**; any other failure, or a response without `policyDocument.Statement`, is **500** (`AuthorizerConfigurationException`). The *policy* is cached — one cache per authorizer per API, keyed by identity values, for `resultTtlInSeconds` when that is greater than 0 — and re-evaluated on every request, so a cached `Resource: event.methodArn` denies the caller's other routes until it expires, exactly as on AWS; hits and misses are on `GET /api/apis/authorizer-cache`. The resulting `principalId`/`context` is injected as `requestContext.authorizer` (values stringified for v1) or `requestContext.authorizer.lambda` for v2.
|
|
450
474
|
|
|
451
475
|
**Where it lives.** `src/server/services/authorizer-service.ts` — identity extraction, event building, caching, policy interpretation. `src/server/services/gateway-manager.ts` — the short-circuit before the handler invoke. `src/server/services/serverless-state-parser.ts` / `raw-api-assembler.ts` — where `AuthorizerConfig` comes from (function events and raw `AWS::ApiGatewayV2::Authorizer`).
|
|
452
476
|
|
|
@@ -485,7 +509,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
485
509
|
<details>
|
|
486
510
|
<summary><b>DynamoDB</b> — tables, expressions, indexes, transactions, TTL and streams, in the same process</summary>
|
|
487
511
|
|
|
488
|
-
**How it works.** Table metadata lives in the `dynamodb/<region>/tables` catalog; items live in one `ItemTable` per table (JSONL snapshot + WAL, hydrated on first touch). A real expression engine (lexer → parser → evaluator) backs `ConditionExpression`, `UpdateExpression`, `ProjectionExpression` and `KeyConditionExpression`, with exact decimal-string arithmetic so `ADD`/`SET a = a + :v` and sort-key comparisons never lose precision. Query/Scan resolve GSIs and LSIs at read time by scanning the base table — index semantics (sparse items, `KEYS_ONLY`/`INCLUDE` projections, ordering, `ExclusiveStartKey`/`LastEvaluatedKey`) are enforced on read, never maintained on write. Limits match AWS: 400 KB per item, 25 `BatchWriteItem` entries, 100 `BatchGetItem` keys, 100 items per transaction, `ListTables` returns 100 by default.
|
|
512
|
+
**How it works.** Table metadata lives in the `dynamodb/<region>/tables` catalog; items live in one `ItemTable` per table (JSONL snapshot + WAL, hydrated on first touch). A real expression engine (lexer → parser → evaluator) backs `ConditionExpression`, `UpdateExpression`, `ProjectionExpression` and `KeyConditionExpression`, with exact decimal-string arithmetic so `ADD`/`SET a = a + :v` and sort-key comparisons never lose precision. Query/Scan resolve GSIs and LSIs at read time by scanning the base table — index semantics (sparse items, `KEYS_ONLY`/`INCLUDE` projections, ordering, `ExclusiveStartKey`/`LastEvaluatedKey`) are enforced on read, never maintained on write. Limits match AWS: 400 KB per item, 25 `BatchWriteItem` entries, 100 `BatchGetItem` keys, 100 items per transaction, `ListTables` returns 100 by default, and a Query/Scan page stops at **1 MB** of examined data (summed before `FilterExpression`) with a `LastEvaluatedKey`, so SDK paginators loop as they do on AWS. The parser enforces AWS's validation too: a bare **reserved word** as an attribute name (`status`, `name`, `data` — the full 573-word list) is a `ValidationException` naming the keyword, `ConsistentRead` on a GSI is rejected, and `a <> :v` is true when `a` is absent (every other comparator false). TTL works on both ends — expired items are invisible and physically removed on read, **and** a 30 s background sweep deletes them so TTL-triggered Lambdas fire without waiting for a read; each deletion emits a `REMOVE` stream record with `userIdentity: {type: "Service", principalId: "dynamodb.amazonaws.com"}`. Each table remembers its earliest pending expiry, so an idle table is not touched (and still idle-unloads) until an item is due. Streams are a per-table in-memory ring of **1000 records** (the local stand-in for the 24 h window) with 21-digit sequence numbers; every append emits `dynamo:stream-appended` on the engine bus.
|
|
489
513
|
|
|
490
514
|
**Where it lives.** `src/server/engine/emulators/dynamodb/index.ts` — control plane, item CRUD, batch and transaction operations. `src/server/engine/emulators/dynamodb/query.ts` — index resolution, key-condition matching, pagination. `src/server/engine/emulators/dynamodb/expressions/` — lexer, parser, evaluator and exact decimal arithmetic. `src/server/engine/emulators/dynamodb/streams.ts` — the change-record ring and the read cursor the tailer consumes.
|
|
491
515
|
|
|
@@ -496,7 +520,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
496
520
|
<details>
|
|
497
521
|
<summary><b>SQS</b> — queues with real visibility timeouts, FIFO ordering, dedup and dead-letter redrive</summary>
|
|
498
522
|
|
|
499
|
-
**How it works.** Served over AWS JSON 1.0 (`X-Amz-Target: AmazonSQS.*`). Queue *attributes* persist in the `sqs/<region>/queues` catalog and are read back with AWS defaults applied — `VisibilityTimeout 30`, `DelaySeconds 0`, `MessageRetentionPeriod 345600`, `ReceiveMessageWaitTimeSeconds 0`, `MaximumMessageSize 262144`. *Messages* are memory-only: a state machine per queue tracks delay deadlines, in-flight visibility, FIFO group serialization and a 5-minute deduplication window, driven by **one unref'd timer per queue** armed to the nearest deadline. Every message that becomes visible emits `sqs:message-visible` on the bus, which is what wakes the delivery loops instead of polling. A `RedrivePolicy` whose `deadLetterTargetArn` resolves to a live queue moves a message to the DLQ
|
|
523
|
+
**How it works.** Served over AWS JSON 1.0 (`X-Amz-Target: AmazonSQS.*`). Queue *attributes* persist in the `sqs/<region>/queues` catalog and are read back with AWS defaults applied — `VisibilityTimeout 30`, `DelaySeconds 0`, `MessageRetentionPeriod 345600`, `ReceiveMessageWaitTimeSeconds 0`, `MaximumMessageSize 262144`. *Messages* are memory-only: a state machine per queue tracks delay deadlines, in-flight visibility, FIFO group serialization and a 5-minute deduplication window, driven by **one unref'd timer per queue** armed to the nearest deadline. Every message that becomes visible emits `sqs:message-visible` on the bus, which is what wakes the delivery loops instead of polling. Receipt handles behave like AWS's: the most recent handle still deletes a message after its visibility lapsed, until someone receives it again — a consumer that finishes just past its timeout deletes what it processed instead of getting a 200 while the message redelivers — and `VisibilityTimeout: 0` makes the message visible again at once, so peeking at a DLQ never misses it. A `RedrivePolicy` whose `deadLetterTargetArn` resolves to a live queue moves a message to the DLQ on the receive that would push its receive count past `maxReceiveCount` (not the instant its visibility lapses), preserving `MessageId` and body; an unresolvable target logs one warning and disables redrive. `ListDeadLetterSourceQueues` answers the reverse question, `ListQueues` pages with `MaxResults`/`NextToken`, `MaximumMessageSize` is enforced on the body plus its attributes, and batch operations enforce the AWS caps (max 10 entries, distinct `Id`s). On graceful shutdown the messages are snapshotted to `sqs-messages.snapshot.json` in the data dir and restored before the delivery loops start.
|
|
500
524
|
|
|
501
525
|
**Where it lives.** `src/server/engine/emulators/sqs/index.ts` — the wire surface, catalog, batch validation and AWS-shaped errors (`AWS.SimpleQueueService.NonExistentQueue`, `QueueAlreadyExists`). `src/server/engine/emulators/sqs/queue.ts` — visibility/delay/FIFO/dedup/redrive state machine. `src/server/engine/emulators/sqs/md5.ts` — the `MD5OfBody`/`MD5OfMessageAttributes` digests SDKs verify.
|
|
502
526
|
|
|
@@ -518,7 +542,7 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
518
542
|
<details>
|
|
519
543
|
<summary><b>S3</b> — buckets and objects on disk, with event notifications, presigned POST and CORS</summary>
|
|
520
544
|
|
|
521
|
-
**How it works.** Path-style rest-xml (the router peels virtual-host `Host` buckets back to path-style first). Bucket metadata lives in the `s3/buckets` catalog, each bucket keeps its object index in its own `ItemTable`, and object bodies are written as **sha256 content-addressed blobs** — bodies never sit in an in-memory map, and identical bytes de-duplicate to one file. Covered: `PutObject`/`GetObject` (with `Range` → 206)/`HeadObject`/`DeleteObject`/`DeleteObjects`/`CopyObject`, `ListObjectsV2` with continuation tokens, bucket versioning + notification + CORS configuration, and browser presigned `POST` via a byte-accurate multipart parser. CORS answers with the matching rule, or a dev-permissive fallback (echo the Origin, allow every method) so uploads work on a fresh bucket. Every write emits `s3:object-event` on the bus; the dispatcher matches it against the bucket's Lambda notification configuration (event globs like `s3:ObjectCreated:*` plus prefix/suffix rules) and delivers the AWS `Records` envelope, retrying twice at 1 s and 3 s before dropping with a warning. Multipart *upload* (`?uploads`/`?uploadId`) is explicitly `NotImplemented` (501), as is `ListObjects` v1.
|
|
545
|
+
**How it works.** Path-style rest-xml (the router peels virtual-host `Host` buckets back to path-style first). Bucket metadata lives in the `s3/buckets` catalog, each bucket keeps its object index in its own `ItemTable`, and object bodies are written as **sha256 content-addressed blobs** — bodies never sit in an in-memory map, and identical bytes de-duplicate to one file. Covered: `PutObject`/`GetObject` (with `Range` → 206)/`HeadObject`/`DeleteObject`/`DeleteObjects`/`CopyObject`, `ListObjectsV2` with continuation tokens, bucket versioning + notification + CORS configuration, bucket **encryption** (a bucket with no rule reports SSE-S3 `AES256`, as AWS does since 2023), **tagging**, **Public Access Block** and **policy** (stored, never enforced), and browser presigned `POST` via a byte-accurate multipart parser. Objects keep their system metadata — `Content-Disposition`, `Cache-Control`, `Content-Encoding`, `Content-Language`, `Expires` — and their server-side-encryption headers across PutObject, presigned POST and CopyObject, and Get/Head return them (bytes are stored unencrypted; the encryption is *reported* exactly as AWS reports it). Every other bucket or object subresource (`?lifecycle`, `?acl`, `?website`, …) answers `NotImplemented` (501) instead of being mistaken for CreateBucket, DeleteBucket or PutObject. CORS answers with the matching rule, or a dev-permissive fallback (echo the Origin, allow every method) so uploads work on a fresh bucket. Every write emits `s3:object-event` on the bus; the dispatcher matches it against the bucket's Lambda notification configuration (event globs like `s3:ObjectCreated:*` plus prefix/suffix rules) and delivers the AWS `Records` envelope, retrying twice at 1 s and 3 s before dropping with a warning. Multipart *upload* (`?uploads`/`?uploadId`) is explicitly `NotImplemented` (501), as is `ListObjects` v1.
|
|
522
546
|
|
|
523
547
|
**Where it lives.** `src/server/engine/emulators/s3/index.ts` — routing, bucket/object operations, notification and CORS configuration. `src/server/engine/emulators/s3/multipart.ts` — the presigned-POST form parser. `src/server/engine/dispatch/dispatcher.ts` — notification matching, `Records` payload and retry policy.
|
|
524
548
|
|
|
@@ -581,6 +605,17 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
581
605
|
|
|
582
606
|
</details>
|
|
583
607
|
|
|
608
|
+
<details>
|
|
609
|
+
<summary><b>SSM Parameter Store</b> — hierarchical config with versions, labels and SecureString, offline</summary>
|
|
610
|
+
|
|
611
|
+
**How it works.** AWS JSON 1.1 (`AmazonSSM.*`), one catalog per region, every version kept (up to AWS's 100). `PutParameter` covers `String`/`StringList`/`SecureString`, `Overwrite` (versions increment), `AllowedPattern`, tiers (Standard 4 KB / Advanced 8 KB / Intelligent-Tiering, no downgrade), `DataType` and create-time tags, with AWS's name rules (fully-qualified hierarchies, max 15 levels, `aws`/`ssm` prefixes reserved). Reads go through `GetParameter`/`GetParameters` (by name or ARN, with `name:version` and `name:label` selectors) and `GetParametersByPath` (`Recursive`, Type/KeyId/DataType/Label filters, 10-per-page pagination); `DescribeParameters`, `GetParameterHistory`, `LabelParameterVersion`, `DeleteParameter(s)` and tagging are covered too. There is no KMS locally: `WithDecryption: true` returns the stored value, and without it a SecureString answers a deterministic opaque token instead of the plaintext, the way AWS answers ciphertext. CloudFormation `AWS::SSM::Parameter` is provisioned at registration (idempotent; an existing parameter is never overwritten).
|
|
612
|
+
|
|
613
|
+
**Where it lives.** `src/server/engine/emulators/ssm/index.ts`; provisioning in `src/server/services/resource-provisioner.ts` (`createSsmParameter`).
|
|
614
|
+
|
|
615
|
+
**Why it exists.** Services read credentials, feature flags and signing keys from Parameter Store paths at cold start; without it every such handler fails at boot or needs a second emulator next to the engine.
|
|
616
|
+
|
|
617
|
+
</details>
|
|
618
|
+
|
|
584
619
|
<details>
|
|
585
620
|
<summary><b>OpenSearch Serverless</b> — collections, indexes and a query-DSL subset, no cluster</summary>
|
|
586
621
|
|
|
@@ -604,9 +639,9 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
|
|
|
604
639
|
</details>
|
|
605
640
|
|
|
606
641
|
<details>
|
|
607
|
-
<summary><b><code>fallbackEndpoint</code></b> — forward what the engine doesn't implement, instead of failing</summary>
|
|
642
|
+
<summary><b><code>fallbackEndpoint</code> / <code>fallbackEndpoints</code></b> — forward what the engine doesn't implement, instead of failing</summary>
|
|
608
643
|
|
|
609
|
-
**How it works.** Unset by default (`null`). When set, any request the engine cannot serve — an unknown service, an unknown `X-Amz-Target` prefix, or a legacy Query-protocol SQS call — is proxied **verbatim** to that endpoint: the original bytes (including `aws-chunked` framing), the original headers minus `Host`, an explicit `Content-Length`, and the upstream response piped
|
|
644
|
+
**How it works.** Unset by default (`null`). When set — in the file, or with `LSS_FALLBACK_ENDPOINT` — any request the engine cannot serve — an unknown service, an unknown `X-Amz-Target` prefix, or a legacy Query-protocol SQS call — is proxied **verbatim** to that endpoint: the original bytes (including `aws-chunked` framing), the original headers minus `Host`, an explicit `Content-Length`, and the upstream response piped back. `selfEngine.fallbackEndpoints` routes individual services elsewhere (`{ "kinesis": "http://localhost:4567" }`, keyed by SigV4 signing name) and wins over the catch-all. On the way back, single-valued headers keep only their first value: an upstream that sends `Date` twice would otherwise reach the SDK as one joined header and break its clock-skew correction from the second call on. If the endpoint is unreachable the caller gets a `ServiceUnavailable` 502 naming it, not a hang. With no fallback configured the same request gets an explicit `NotImplemented` **in the caller's protocol** (AWS JSON, Query XML, S3-style XML or rest-json, so every SDK shows a readable error) naming `service.Operation` and linking to the coverage table on GitHub — the engine never silently succeeds. Gaps *inside* a service the engine does serve are a different error that deliberately does **not** mention the fallback, because forwarding them would read the wrong state.
|
|
610
645
|
|
|
611
646
|
**Where it lives.** `src/server/engine/http/router.ts` — `respondUnknown` / `proxyToFallback` and the legacy-SQS branch. `src/server/engine/http/errors.ts` — the two distinct `NotImplemented` messages. `src/server/services/config-manager.ts` — `selfEngine.fallbackEndpoint` resolution.
|
|
612
647
|
|
|
@@ -826,6 +861,17 @@ red/green are 4.4 ΔE apart under deuteranopia.
|
|
|
826
861
|
|
|
827
862
|
</details>
|
|
828
863
|
|
|
864
|
+
<details>
|
|
865
|
+
<summary><b>Service wiring diagram</b> — the service detail screen draws what talks to what, and says how it knows</summary>
|
|
866
|
+
|
|
867
|
+
**How it works.** `GET /api/services/:name/graph` derives the diagram from the packaged CloudFormation template the registrar cached plus that service's registration metadata — never from the running engine, so a stopped service answers exactly like a running one and no arrow disappears because a worker went cold. Nodes are the service's declared resources (Lambda, DynamoDB, SQS, SNS, S3, EventBridge bus and rule, OpenSearch, Secrets Manager), one node **per HTTP route** (`POST /orders`, not a single API box — which function serves which path is the first thing this screen is for), the execution role when more than one function shares it, and an `external` node for every ARN or name the service references but does not declare, which is where the cross-service links surface. Every edge carries a kind and a **confidence**. `declared` means CloudFormation (or `serverless-state.json`) wires it and the provisioner acts on it: HTTP route → function, authorizer → route, event-source mapping (SQS poll or DynamoDB stream, with batch size), S3 notification (with the event names), bus → rule → target, SQS redrive (with `maxReceiveCount`), SNS subscription (with the protocol) and IAM policy grant (with the granted actions). `inferred` covers exactly one case — an environment variable whose value names a declared resource, matched as a whole name, as an ARN or as the last segment of a service URL, **never** as a substring — and those edges are additionally flagged `serviceWide` when the identical key/value sits on every function of the service, i.e. it came from `provider.environment`, was copied onto each function by the packager and therefore says nothing about the one it is drawn on. IAM is the other place the drawing is deliberately not literal: the framework emits **one** execution role shared by every function, so a grant on it means "every Lambda bound to this role may touch that table" — with one function on the role the edge is drawn straight to the resource, with several the role becomes a node and the grants read as N + M edges through a hub instead of an N × M fan-out (at ≈60 functions × ≈10 tables the product form is 600 arrows of noise, and the hub is what the template literally says). Framework noise is dropped on purpose — `Resource: "*"` grants, `logs:`/`xray:`/`sts:` actions, the custom-resources helper role, the API Gateway v2 plumbing resources and endpoint-shaped env vars like `AWS_ENDPOINT` — and an IAM statement that merely restates an event-source mapping lands on the existing arrow instead of doubling it. Anything that could not be resolved comes back in `warnings[]` rather than vanishing. The endpoint answers **400** for an invalid name, **404** for an unknown service, and an empty graph carrying one warning when the service is registered but has no cached template. The same payload is on `client.services.graph(name)` and the `lss_service_graph` MCP tool.
|
|
868
|
+
|
|
869
|
+
**Where it lives.** `src/server/services/service-graph.ts` — `buildServiceGraph()`, pure and I/O-free, plus `parseResourceArn()` and the node/edge types. `src/server/routes/services.ts` — `GET /:name/graph`. `src/ui/src/components/graph/` — the layout and the canvas drawing, hosted by `src/ui/src/pages/ServiceDetailPage.vue`. `src/ui/src/services/api.ts` — the typed call. `src/client/namespaces/services.ts` (`services.graph(name)`) and `src/mcp/tools.ts` (`lss_service_graph`) — the same graph for a test or an agent.
|
|
870
|
+
|
|
871
|
+
**Why it exists.** The detail page already answered "what does this service own"; "and what is wired to what" still meant reading a packaged template by hand — which is exactly where the interesting arrows hide, since a rule listening on another service's bus or a grant on someone else's queue is the reason a monorepo runs one stack instead of fifteen. The confidence flag is the payload's point rather than a footnote: an env-var name match is a guess, and a diagram that draws a guess like a declared wire is worse than no diagram at all.
|
|
872
|
+
|
|
873
|
+
</details>
|
|
874
|
+
|
|
829
875
|
<details>
|
|
830
876
|
<summary><b>Lambdas tab</b> — list every function and invoke it with a JSON payload from the browser</summary>
|
|
831
877
|
|
|
@@ -939,7 +985,7 @@ red/green are 4.4 ΔE apart under deuteranopia.
|
|
|
939
985
|
<details>
|
|
940
986
|
<summary><b>Trilingual UI</b> — the whole dashboard in English, Brazilian Portuguese or Spanish</summary>
|
|
941
987
|
|
|
942
|
-
**How it works.** A ~40-line hand-rolled i18n layer (no `vue-i18n` dependency) exposes a reactive `t(key, params)` with `{placeholder}` interpolation. Catalogues live one file per feature area under `i18n/messages/`, each carrying all three languages side by side and flattened into `area.key` — currently **794 keys per locale**. The initial locale is the stored choice, else the best match from `navigator.languages` (`pt` → `pt-BR`, `es-AR` → `es`), else English; the `⋮` menu switches it with a ✓ on the active one, persists to `localStorage` and
|
|
988
|
+
**How it works.** A ~40-line hand-rolled i18n layer (no `vue-i18n` dependency) exposes a reactive `t(key, params)` with `{placeholder}` interpolation. Catalogues live one file per feature area under `i18n/messages/`, each carrying all three languages side by side and flattened into `area.key` — currently **794 keys per locale**. The initial locale is the stored choice, else the best match from `navigator.languages` (`pt` → `pt-BR`, `es-AR` → `es`), else English; whichever wins is written to `document.documentElement.lang` **at boot** — `index.html` can only ship a static `lang="en"`, and a translated page left announcing itself as English is read aloud in the wrong language. The `⋮` menu switches it with a ✓ on the active one, persists to `localStorage` and updates `<html lang>` again. A missing key falls back to English and finally renders the key itself, so an untranslated screen still works. AWS vocabulary (service names, `Scan`/`Query`, attribute types, `RequestResponse`) is deliberately left untranslated; server scan warnings arrive with a stable `code` the dashboard translates, falling back to the server's English message for codes it does not know.
|
|
943
989
|
|
|
944
990
|
**Where it lives.** `src/ui/src/i18n/index.ts` — locale detection, storage, reactive `t()`. `src/ui/src/i18n/messages/index.ts` — area merge into the flat per-locale map. `src/ui/src/i18n/messages/*.ts` — 13 area catalogues (common, nav, onboarding, overview, services, lambdas, apis, queues, dynamo, buckets, opensearch, secrets, settings).
|
|
945
991
|
|
|
@@ -961,13 +1007,13 @@ red/green are 4.4 ΔE apart under deuteranopia.
|
|
|
961
1007
|
</details>
|
|
962
1008
|
|
|
963
1009
|
<details>
|
|
964
|
-
<summary><b>MCP server (`lss mcp`) —
|
|
1010
|
+
<summary><b>MCP server (`lss mcp`) — 26 tools</b> — hands an AI coding agent read and write access to the running stack.</summary>
|
|
965
1011
|
|
|
966
|
-
**How it works.** `lss mcp` dynamic-imports the ESM build at `dist/mcp/server.js` and speaks newline-delimited JSON-RPC 2.0 on stdin/stdout — protocol revision `2024-11-05`, advertising `capabilities.tools.listChanged: false`. It implements `initialize`, `ping`, `tools/list` and `tools/call`; unimplemented *notifications* are silently accepted per spec, while a request for an unknown method gets `-32601`. Incoming lines are processed through a promise chain so responses come back in arrival order, and stdout carries protocol frames only (diagnostics go to stderr). The
|
|
1012
|
+
**How it works.** `lss mcp` dynamic-imports the ESM build at `dist/mcp/server.js` and speaks newline-delimited JSON-RPC 2.0 on stdin/stdout — protocol revision `2024-11-05`, advertising `capabilities.tools.listChanged: false`. It implements `initialize`, `ping`, `tools/list` and `tools/call`; unimplemented *notifications* are silently accepted per spec, while a request for an unknown method gets `-32601`. Incoming lines are processed through a promise chain so responses come back in arrival order, and stdout carries protocol frames only (diagnostics go to stderr). The 26 tools are thin wrappers over the same REST API the client uses — no AWS SDK in the process — and five of them are flagged `MUTATES` in their description because the MCP client shows that text to a human before approving: `lss_register_service`, `lss_invoke`, `lss_dynamo_put_item`, `lss_queue_send`, `lss_seed_run`. A failing tool answers a text result with `isError: true` (a normal outcome the model reads and reacts to) rather than a transport-level JSON-RPC error, and every result is pretty-printed JSON text. The HTTP layer here uses a 130 s timeout — deliberately above `lss_queue_await_idle`'s 120 s server-side clamp — and treats 408 as a result, not a failure.
|
|
967
1013
|
|
|
968
|
-
**Where it lives.** `src/mcp/tools.ts` — the
|
|
1014
|
+
**Where it lives.** `src/mcp/tools.ts` — the 26 tool definitions, their JSON schemas and the HTTP call each makes. `src/mcp/protocol.ts` — hand-rolled JSON-RPC dispatch (`handleMessage`/`handleLine`), pure and testable. `src/mcp/server.ts` — the stdio seam: readline in, `JSON.stringify` frames out. `src/mcp/http.ts` — base-URL resolution and the fetch caller with `LssUnreachableError`.
|
|
969
1015
|
|
|
970
|
-
**Why it exists.** An agent debugging a serverless monorepo otherwise guesses at local state; with this it can ask what's provisioned (`lss_health`, `lss_resources`, `lss_resource_owners`, `lss_apis`), read data (`lss_dynamo_scan`/`_query`, `lss_bucket_objects`, `lss_opensearch_search`, `lss_secrets`, `lss_lambda_logs`), and then act (`lss_invoke`, `lss_queue_send`, `lss_seed_run`) against the real local stack. The official MCP SDK is skipped on purpose: stdio JSON-RPC is a few dozen lines, and the package keeps a deliberately small dependency tree. The repo's own `.mcp.json` wires it up as `node ./bin/cli.js mcp`.
|
|
1016
|
+
**Why it exists.** An agent debugging a serverless monorepo otherwise guesses at local state; with this it can ask what's provisioned (`lss_health`, `lss_resources`, `lss_resource_owners`, `lss_apis`) and how one service is wired (`lss_service_graph`), read data (`lss_dynamo_scan`/`_query`, `lss_bucket_objects`, `lss_opensearch_search`, `lss_secrets`, `lss_lambda_logs`), and then act (`lss_invoke`, `lss_queue_send`, `lss_seed_run`) against the real local stack. The official MCP SDK is skipped on purpose: stdio JSON-RPC is a few dozen lines, and the package keeps a deliberately small dependency tree. The repo's own `.mcp.json` wires it up as `node ./bin/cli.js mcp`.
|
|
971
1017
|
|
|
972
1018
|
</details>
|
|
973
1019
|
|
|
@@ -1042,9 +1088,9 @@ red/green are 4.4 ΔE apart under deuteranopia.
|
|
|
1042
1088
|
<details>
|
|
1043
1089
|
<summary><b>Per-service overrides</b> — one service can package or run differently without forking the global settings</summary>
|
|
1044
1090
|
|
|
1045
|
-
**How it works.** `servicePackaging` and `serviceRuntime` are maps keyed by service. A lookup tries the keys most-specific-first: the service path relative to the config file's directory, then relative to the project root, then the plain directory basename — both relative spellings are computed because a symlinked checkout or a `~`-loaded config makes them differ, which used to drop overrides silently. Packaging merges as: per-service `packageCommand`/`packageTimeoutMs` **replace** the global value, `packageArgs` are **appended** after the global args, and `packageEnv` is merged over the global (service wins). Runtime merges `enabled`, `execution`, `watch`, plus explicit `apiPort`/`invokePort` over the global `lambdaRuntime` block; `watch` is left undefined when unset so the runtime can apply its mode-dependent default (on for `source`, off for `artifact`).
|
|
1091
|
+
**How it works.** `servicePackaging` and `serviceRuntime` are maps keyed by service. A lookup tries the keys most-specific-first: the service path relative to the config file's directory, then relative to the project root, then the plain directory basename — both relative spellings are computed because a symlinked checkout or a `~`-loaded config makes them differ, which used to drop overrides silently. Packaging merges as: per-service `packageCommand`/`packageTimeoutMs` **replace** the global value, `packageArgs` are **appended** after the global args, and `packageEnv` is merged over the global (service wins). Runtime merges `enabled`, `execution`, `watch`, plus explicit `apiPort`/`invokePort` over the global `lambdaRuntime` block; `watch` is left undefined when unset so the runtime can apply its mode-dependent default (on for `source`, off for `artifact`). A runtime entry can also carry worker overrides: `env` (extra variables for every function of the service, re-read per invocation so an edit applies without a restart), `execArgv` (Node flags for the worker's next fork — `--inspect=127.0.0.1:9230`, `--enable-source-maps`, heap sizing; the API accepts only an allowlist) and `functions.<fn>.env` (one function); precedence is function > service > the template's `environment`, and `GET /api/config` shows only the variable names.
|
|
1046
1092
|
|
|
1047
|
-
**Where it lives.** `src/server/services/config-manager.ts` — `serviceEntryKeys()`, `lookupServiceEntry()`, `getPackageConfigForService()`, `getRuntimeConfigForService()`.
|
|
1093
|
+
**Where it lives.** `src/server/services/config-manager.ts` — `serviceEntryKeys()`, `lookupServiceEntry()`, `getPackageConfigForService()`, `getRuntimeConfigForService()`, `getWorkerOverridesForService()`. `src/server/services/worker-runtime.ts` — env layering and the `execArgv` allowlist.
|
|
1048
1094
|
|
|
1049
1095
|
**Why it exists.** In a 40-service monorepo one service inevitably needs a different package command, an extra `--param`, a pinned port or source-mode execution. Without per-service keys the choice is global (wrong for 39 services) or a hand-run command outside the stack.
|
|
1050
1096
|
|
|
@@ -1077,14 +1123,14 @@ npx lss start
|
|
|
1077
1123
|
```
|
|
1078
1124
|
|
|
1079
1125
|
```
|
|
1080
|
-
|
|
1126
|
+
AWS_ENDPOINT_URL=http://localhost:14566 # point your services here, done (LSS-run handlers get it automatically)
|
|
1081
1127
|
```
|
|
1082
1128
|
|
|
1083
1129
|
```mermaid
|
|
1084
1130
|
flowchart LR
|
|
1085
1131
|
APP[Your services<br/>AWS SDK → :14566] --> ENG
|
|
1086
1132
|
subgraph ORCH["One orchestrator process — no Docker"]
|
|
1087
|
-
ENG[Self engine<br/>DynamoDB · SQS · S3 · EventBridge<br/>OpenSearch · Secrets Manager · SNS · STS]
|
|
1133
|
+
ENG[Self engine<br/>DynamoDB · SQS · S3 · EventBridge<br/>OpenSearch · Secrets Manager · SSM · SNS · STS]
|
|
1088
1134
|
ENG -->|in-process events| RT[LSS Lambda runtime<br/>your handlers, ports 30xx/130xx]
|
|
1089
1135
|
ENG --- FS[(local files<br/>~/.lss/engine)]
|
|
1090
1136
|
end
|
|
@@ -1158,6 +1204,7 @@ npx lss status # is it running? which ports?
|
|
|
1158
1204
|
npx lss logs # tail the orchestrator log
|
|
1159
1205
|
npx lss scan # list Serverless/osls services under the project root
|
|
1160
1206
|
npx lss register [path...] # register services (defaults to the current directory)
|
|
1207
|
+
npx lss register --repackage # ...building them first, instead of registering the artifacts on disk
|
|
1161
1208
|
npx lss seed [table] # apply DynamoDB seed files (all tables or one)
|
|
1162
1209
|
npx lss seed:clear [table] -y # empty seeded tables (prompts unless --yes)
|
|
1163
1210
|
npx lss stop # stop the orchestrator
|
|
@@ -1253,8 +1300,10 @@ Boot prints a warning naming exactly what you opened. Full detail:
|
|
|
1253
1300
|
|
|
1254
1301
|
The Vue 3 dashboard at `http://localhost:14566` has ten tabs — **Overview** (health,
|
|
1255
1302
|
config snapshot, exposed-ports map), **Services** (status, start/stop, live logs,
|
|
1256
|
-
per-service resource breakdown incl. EventBridge buses & rules
|
|
1257
|
-
|
|
1303
|
+
per-service resource breakdown incl. EventBridge buses & rules, plus a wiring diagram
|
|
1304
|
+
of what the service declared talking to what — declared wires and guessed env-var
|
|
1305
|
+
matches drawn apart), **Lambdas** (registry + invoke), **APIs** (emulated HTTP
|
|
1306
|
+
routes), **Queues** (send/receive/purge, consumers),
|
|
1258
1307
|
**S3** (objects, upload/download), **DynamoDB** (items explorer, editor, TTL, seeds),
|
|
1259
1308
|
**OpenSearch**, **Secrets**, and **Settings** — which edits `lss.config.json` in place
|
|
1260
1309
|
(only changed fields are written, so you review and commit the diff), hot-reloads what it
|