local-serverless-stack 1.0.0 → 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.
Files changed (200) hide show
  1. package/CHANGELOG.md +125 -3
  2. package/README.md +92 -38
  3. package/bin/cli.js +218 -28
  4. package/bin/i18n.js +57 -3
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/namespaces/services.d.ts +46 -0
  8. package/dist/client/namespaces/services.d.ts.map +1 -1
  9. package/dist/client/namespaces/services.js +1 -0
  10. package/dist/client/namespaces/services.js.map +1 -1
  11. package/dist/mcp/tools.d.ts.map +1 -1
  12. package/dist/mcp/tools.js +17 -0
  13. package/dist/mcp/tools.js.map +1 -1
  14. package/dist/server/engine/backends/self-backend.d.ts +26 -1
  15. package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
  16. package/dist/server/engine/backends/self-backend.js +104 -11
  17. package/dist/server/engine/backends/self-backend.js.map +1 -1
  18. package/dist/server/engine/emulators/dynamodb/expressions/parser.d.ts.map +1 -1
  19. package/dist/server/engine/emulators/dynamodb/expressions/parser.js +12 -1
  20. package/dist/server/engine/emulators/dynamodb/expressions/parser.js.map +1 -1
  21. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts +3 -0
  22. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts.map +1 -0
  23. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js +78 -0
  24. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js.map +1 -0
  25. package/dist/server/engine/emulators/dynamodb/index.d.ts +11 -0
  26. package/dist/server/engine/emulators/dynamodb/index.d.ts.map +1 -1
  27. package/dist/server/engine/emulators/dynamodb/index.js +101 -7
  28. package/dist/server/engine/emulators/dynamodb/index.js.map +1 -1
  29. package/dist/server/engine/emulators/dynamodb/query.d.ts +1 -0
  30. package/dist/server/engine/emulators/dynamodb/query.d.ts.map +1 -1
  31. package/dist/server/engine/emulators/dynamodb/query.js +34 -6
  32. package/dist/server/engine/emulators/dynamodb/query.js.map +1 -1
  33. package/dist/server/engine/emulators/s3/index.d.ts +10 -0
  34. package/dist/server/engine/emulators/s3/index.d.ts.map +1 -1
  35. package/dist/server/engine/emulators/s3/index.js +369 -25
  36. package/dist/server/engine/emulators/s3/index.js.map +1 -1
  37. package/dist/server/engine/emulators/sqs/index.d.ts +2 -0
  38. package/dist/server/engine/emulators/sqs/index.d.ts.map +1 -1
  39. package/dist/server/engine/emulators/sqs/index.js +70 -7
  40. package/dist/server/engine/emulators/sqs/index.js.map +1 -1
  41. package/dist/server/engine/emulators/sqs/queue.d.ts +2 -1
  42. package/dist/server/engine/emulators/sqs/queue.d.ts.map +1 -1
  43. package/dist/server/engine/emulators/sqs/queue.js +68 -35
  44. package/dist/server/engine/emulators/sqs/queue.js.map +1 -1
  45. package/dist/server/engine/emulators/ssm/index.d.ts +31 -0
  46. package/dist/server/engine/emulators/ssm/index.d.ts.map +1 -0
  47. package/dist/server/engine/emulators/ssm/index.js +670 -0
  48. package/dist/server/engine/emulators/ssm/index.js.map +1 -0
  49. package/dist/server/engine/http/errors.d.ts +1 -0
  50. package/dist/server/engine/http/errors.d.ts.map +1 -1
  51. package/dist/server/engine/http/errors.js +13 -5
  52. package/dist/server/engine/http/errors.js.map +1 -1
  53. package/dist/server/engine/http/is-aws-request.d.ts.map +1 -1
  54. package/dist/server/engine/http/is-aws-request.js +13 -3
  55. package/dist/server/engine/http/is-aws-request.js.map +1 -1
  56. package/dist/server/engine/http/protocols/aws-json.d.ts +1 -0
  57. package/dist/server/engine/http/protocols/aws-json.d.ts.map +1 -1
  58. package/dist/server/engine/http/protocols/aws-json.js +13 -0
  59. package/dist/server/engine/http/protocols/aws-json.js.map +1 -1
  60. package/dist/server/engine/http/reset.d.ts +4 -0
  61. package/dist/server/engine/http/reset.d.ts.map +1 -0
  62. package/dist/server/engine/http/reset.js +17 -0
  63. package/dist/server/engine/http/reset.js.map +1 -0
  64. package/dist/server/engine/http/router.d.ts +1 -0
  65. package/dist/server/engine/http/router.d.ts.map +1 -1
  66. package/dist/server/engine/http/router.js +185 -34
  67. package/dist/server/engine/http/router.js.map +1 -1
  68. package/dist/server/engine/types.d.ts +2 -1
  69. package/dist/server/engine/types.d.ts.map +1 -1
  70. package/dist/server/routes/apis.d.ts.map +1 -1
  71. package/dist/server/routes/apis.js +6 -0
  72. package/dist/server/routes/apis.js.map +1 -1
  73. package/dist/server/routes/config.d.ts.map +1 -1
  74. package/dist/server/routes/config.js +25 -1
  75. package/dist/server/routes/config.js.map +1 -1
  76. package/dist/server/routes/lambdas.d.ts.map +1 -1
  77. package/dist/server/routes/lambdas.js +12 -0
  78. package/dist/server/routes/lambdas.js.map +1 -1
  79. package/dist/server/routes/services.d.ts.map +1 -1
  80. package/dist/server/routes/services.js +66 -5
  81. package/dist/server/routes/services.js.map +1 -1
  82. package/dist/server/runtime/runtime-worker.js +40 -3
  83. package/dist/server/runtime/runtime-worker.js.map +1 -1
  84. package/dist/server/services/api-gateway-events.d.ts +14 -0
  85. package/dist/server/services/api-gateway-events.d.ts.map +1 -1
  86. package/dist/server/services/api-gateway-events.js +114 -8
  87. package/dist/server/services/api-gateway-events.js.map +1 -1
  88. package/dist/server/services/artifact-staleness.d.ts +20 -0
  89. package/dist/server/services/artifact-staleness.d.ts.map +1 -0
  90. package/dist/server/services/artifact-staleness.js +115 -0
  91. package/dist/server/services/artifact-staleness.js.map +1 -0
  92. package/dist/server/services/authorizer-service.d.ts +34 -5
  93. package/dist/server/services/authorizer-service.d.ts.map +1 -1
  94. package/dist/server/services/authorizer-service.js +0 -0
  95. package/dist/server/services/authorizer-service.js.map +1 -1
  96. package/dist/server/services/cache-manager.d.ts +5 -0
  97. package/dist/server/services/cache-manager.d.ts.map +1 -1
  98. package/dist/server/services/cache-manager.js.map +1 -1
  99. package/dist/server/services/cfn-intrinsics.d.ts +63 -0
  100. package/dist/server/services/cfn-intrinsics.d.ts.map +1 -0
  101. package/dist/server/services/cfn-intrinsics.js +403 -0
  102. package/dist/server/services/cfn-intrinsics.js.map +1 -0
  103. package/dist/server/services/cfn-physical-values.d.ts +51 -0
  104. package/dist/server/services/cfn-physical-values.d.ts.map +1 -0
  105. package/dist/server/services/cfn-physical-values.js +186 -0
  106. package/dist/server/services/cfn-physical-values.js.map +1 -0
  107. package/dist/server/services/cloudformation-parser.d.ts +26 -3
  108. package/dist/server/services/cloudformation-parser.d.ts.map +1 -1
  109. package/dist/server/services/cloudformation-parser.js +79 -7
  110. package/dist/server/services/cloudformation-parser.js.map +1 -1
  111. package/dist/server/services/config-manager.d.ts +49 -2
  112. package/dist/server/services/config-manager.d.ts.map +1 -1
  113. package/dist/server/services/config-manager.js +206 -24
  114. package/dist/server/services/config-manager.js.map +1 -1
  115. package/dist/server/services/gateway-manager.d.ts.map +1 -1
  116. package/dist/server/services/gateway-manager.js +52 -20
  117. package/dist/server/services/gateway-manager.js.map +1 -1
  118. package/dist/server/services/lambda-runtime-manager.d.ts +2 -0
  119. package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
  120. package/dist/server/services/lambda-runtime-manager.js +49 -15
  121. package/dist/server/services/lambda-runtime-manager.js.map +1 -1
  122. package/dist/server/services/raw-api-assembler.d.ts +1 -1
  123. package/dist/server/services/raw-api-assembler.d.ts.map +1 -1
  124. package/dist/server/services/raw-api-assembler.js +18 -8
  125. package/dist/server/services/raw-api-assembler.js.map +1 -1
  126. package/dist/server/services/resource-provisioner.d.ts +31 -1
  127. package/dist/server/services/resource-provisioner.d.ts.map +1 -1
  128. package/dist/server/services/resource-provisioner.js +257 -25
  129. package/dist/server/services/resource-provisioner.js.map +1 -1
  130. package/dist/server/services/s3-explorer.js +1 -1
  131. package/dist/server/services/seed-manager.d.ts.map +1 -1
  132. package/dist/server/services/seed-manager.js +11 -6
  133. package/dist/server/services/seed-manager.js.map +1 -1
  134. package/dist/server/services/serverless-state-parser.d.ts +45 -2
  135. package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
  136. package/dist/server/services/serverless-state-parser.js +217 -29
  137. package/dist/server/services/serverless-state-parser.js.map +1 -1
  138. package/dist/server/services/service-graph.d.ts +76 -0
  139. package/dist/server/services/service-graph.d.ts.map +1 -0
  140. package/dist/server/services/service-graph.js +914 -0
  141. package/dist/server/services/service-graph.js.map +1 -0
  142. package/dist/server/services/service-registrar.d.ts +17 -1
  143. package/dist/server/services/service-registrar.d.ts.map +1 -1
  144. package/dist/server/services/service-registrar.js +190 -66
  145. package/dist/server/services/service-registrar.js.map +1 -1
  146. package/dist/server/services/service-scanner.d.ts +7 -2
  147. package/dist/server/services/service-scanner.d.ts.map +1 -1
  148. package/dist/server/services/service-scanner.js +53 -7
  149. package/dist/server/services/service-scanner.js.map +1 -1
  150. package/dist/server/services/source-watcher.d.ts +1 -0
  151. package/dist/server/services/source-watcher.d.ts.map +1 -1
  152. package/dist/server/services/source-watcher.js +19 -1
  153. package/dist/server/services/source-watcher.js.map +1 -1
  154. package/dist/server/services/worker-runtime.d.ts +82 -0
  155. package/dist/server/services/worker-runtime.d.ts.map +1 -0
  156. package/dist/server/services/worker-runtime.js +303 -0
  157. package/dist/server/services/worker-runtime.js.map +1 -0
  158. package/dist/ui/assets/ApisPage-BkW-QZ0X.js +1 -0
  159. package/dist/ui/assets/BucketDetailPage-CJwTQJh7.js +1 -0
  160. package/dist/ui/assets/BucketsPage-B1xPVhZC.js +1 -0
  161. package/dist/ui/assets/DynamoPage-DvbpwZDc.js +1 -0
  162. package/dist/ui/assets/DynamoTablePage-xn3Lq3so.js +3 -0
  163. package/dist/ui/assets/LambdaDetailPage-BhUyO77H.js +4 -0
  164. package/dist/ui/assets/LambdasPage-CUF_4rOH.js +1 -0
  165. package/dist/ui/assets/OnboardingPage-U49THUzu.js +1 -0
  166. package/dist/ui/assets/OpenSearchCollectionPage-BJTVNCq4.js +1 -0
  167. package/dist/ui/assets/OpenSearchPage-4s-egkB9.js +1 -0
  168. package/dist/ui/assets/OverviewPage-DJeJQktG.js +1 -0
  169. package/dist/ui/assets/{OverviewPage-C1pBJ8aP.css → OverviewPage-DjQETmb_.css} +1 -1
  170. package/dist/ui/assets/QueueDetailPage-DWsy5Yt8.js +1 -0
  171. package/dist/ui/assets/QueuesPage-BZJSqman.js +1 -0
  172. package/dist/ui/assets/SecretsPage-YC9ua54G.js +1 -0
  173. package/dist/ui/assets/ServiceDetailPage-Cum5frP4.js +2 -0
  174. package/dist/ui/assets/ServicesPage-IpaPDuqx.js +2 -0
  175. package/dist/ui/assets/SettingsPage-BiCRU8O6.js +1 -0
  176. package/dist/ui/assets/index-BDlrAJhi.css +1 -0
  177. package/dist/ui/assets/index-BouKSS43.js +38 -0
  178. package/dist/ui/assets/resourceIcons-DZs28nbs.js +1 -0
  179. package/dist/ui/index.html +5 -2
  180. package/package.json +1 -1
  181. package/dist/ui/assets/ApisPage-BOuimwWz.js +0 -1
  182. package/dist/ui/assets/BucketDetailPage-vLYwhoQt.js +0 -1
  183. package/dist/ui/assets/BucketsPage-Cu5lU1Hp.js +0 -1
  184. package/dist/ui/assets/DynamoPage-y3TTEPUL.js +0 -1
  185. package/dist/ui/assets/DynamoTablePage-BnmSRZjc.js +0 -3
  186. package/dist/ui/assets/LambdaDetailPage-B5q_d27t.js +0 -4
  187. package/dist/ui/assets/LambdasPage-CJpRoEDx.js +0 -1
  188. package/dist/ui/assets/OnboardingPage-D02dymQi.js +0 -1
  189. package/dist/ui/assets/OpenSearchCollectionPage-B_wWTlfL.js +0 -1
  190. package/dist/ui/assets/OpenSearchPage-BLEgfF_g.js +0 -1
  191. package/dist/ui/assets/OverviewPage-EdJWiAt6.js +0 -1
  192. package/dist/ui/assets/QueueDetailPage-ByEYHvHe.js +0 -1
  193. package/dist/ui/assets/QueuesPage-CvwYHSll.js +0 -1
  194. package/dist/ui/assets/SecretsPage-DASw8Ntf.js +0 -1
  195. package/dist/ui/assets/ServiceDetailPage-BMsUDg6L.js +0 -2
  196. package/dist/ui/assets/ServicesPage-2JuiIiRr.js +0 -2
  197. package/dist/ui/assets/SettingsPage-DStO4bgN.js +0 -1
  198. package/dist/ui/assets/index-BIEznsi4.js +0 -37
  199. package/dist/ui/assets/index-BRMvNP3Y.css +0 -1
  200. package/dist/ui/assets/resourceIcons-baecADXA.js +0 -1
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). 86 features in nine areas — start with [Quick Start](#quick-start) if you just
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
@@ -45,18 +45,20 @@ want it running.
45
45
  <details>
46
46
  <summary><b>Background orchestrator lifecycle</b> — <code>lss start</code> / <code>stop</code> / <code>status</code> run the whole stack as a detached process you can forget about</summary>
47
47
 
48
- **How it works.** `start` checks the PID file first: if the recorded PID answers `kill(pid, 0)` it just reprints the URLs and returns; a dead PID file is deleted and boot continues. It resolves `dist/server/index.js`, opens the log file in append mode and `spawn`s `node` detached with stdio piped to that file, `unref`s the child and writes its PID. Ports come from config plus env (`serverPort` and the self engine both default to **14566**; the DynamoDB proxy is off, port **8000** when enabled), and they are passed down as `PORT`, `LSS_ENABLE_DYNAMO_PROXY`, `LSS_DYNAMO_PROXY_PORT`. Two seconds later it re-probes the PID and prints either "Service is running" or a failure pointing at the log. `stop` sends `SIGTERM` and then polls `kill(pid, 0)` every **200 ms for up to 10 s** before deleting the PID file — without that wait an immediate `lss start` would race the dying process for the port and die with `EADDRINUSE`. `status` prints PID, server URL, self engine URL, proxy URL (only when enabled) and log path, and self-heals a stale PID file.
48
+ **How it works.** `start` checks the PID file first: if the recorded PID answers `kill(pid, 0)` it just reprints the URLs and returns; a dead PID file is deleted and boot continues. It resolves `dist/server/index.js`, opens the log file in append mode and `spawn`s `node` detached with stdio piped to that file, `unref`s the child and writes its PID. Ports come from config plus env (`serverPort` and the self engine both default to **14566**; the DynamoDB proxy is off, port **8000** when enabled), and they are passed down as `PORT`, `LSS_ENABLE_DYNAMO_PROXY`, `LSS_DYNAMO_PROXY_PORT`. Two seconds later it re-probes the PID: alive prints "Service is running", **dead prints the failure, the tail of the log, who holds the port — and exits 1**, because a daemonized boot discovers a busy port only after the CLI has printed a PID, and a step that fails must not report success. `lss start --wait[=<seconds>]` (default 30) goes further and blocks until `GET /api/health` answers, which is what a sequential chain needs so the next `register` isn't run against a stack that never came up. `stop` sends `SIGTERM` and then polls `kill(pid, 0)` every **200 ms for up to 10 s** before deleting the PID file — without that wait an immediate `lss start` would race the dying process for the port and die with `EADDRINUSE`. `status` prints PID, server URL, self engine URL, proxy URL (only when enabled) and log path, and self-heals a stale PID file. When there is **no** PID file for this project it asks `GET /api/config` who is on the port and reports `port 14566 is in use by the orchestrator of /other/project` instead of `NOT RUNNING` — under `--net=host` the port belongs to the whole machine, and a status that says "not running" about a port answering 200 sends you looking in the wrong place.
49
49
 
50
- **Where it lives.** `bin/cli.js` — `startOrchestrator()`, `stopOrchestrator()`, `waitForExit()`, `showStatus()`, `runtimePaths()`. `src/server/index.ts` — the orchestrator entry point that gets spawned.
50
+ **Where it lives.** `bin/cli.js` — `startOrchestrator()`, `waitForHealthy()`, `reportStartFailure()`, `probePortOwner()`, `stopOrchestrator()`, `waitForExit()`, `showStatus()`, `runtimePaths()`. `src/server/index.ts` — the orchestrator entry point that gets spawned.
51
51
 
52
- **Why it exists.** One command replaces "start LocalStack, then start one `serverless-offline` per service". Because the process is detached and PID-tracked, the stack survives closing the terminal, and `status` answers the "is it up?" question without grepping `ps`.
52
+ **Why it exists.** One command replaces "start LocalStack, then start one `serverless-offline` per service". Because the process is detached and PID-tracked, the stack survives closing the terminal, and `status` answers the "is it up?" question — and, when the answer is no, the "then who has my port?" question — without grepping `ps`.
53
53
 
54
54
  </details>
55
55
 
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; 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
 
@@ -67,7 +69,7 @@ want it running.
67
69
  <details>
68
70
  <summary><b><code>lss scan</code></b> — list every Serverless/osls service under the project root with its readiness flags</summary>
69
71
 
70
- **How it works.** It calls `GET /api/services/scan`, which walks the project root (skipping `node_modules`, `.git`, `.serverless`, `.lss`, `.build`, `.esbuild`) and returns one row per service. Each row prints three flags — `registered` (known to this orchestrator), `installed` (a `node_modules` is resolvable from the service dir upward to the root, so packaging can run without an install) and `packaged` (`.serverless/cloudformation-template-update-stack.json` exists) — followed by the resolved `api:`/`invoke:` ports when known. A second line shows the effective package command for that service (per-service `servicePackaging`, else the global one), and any scan warnings are listed underneath. Empty results print the root that was searched.
72
+ **How it works.** It calls `GET /api/services/scan`, which walks the project root (skipping `node_modules`, `.git`, `.serverless`, `.lss`, `.build`, `.esbuild`, plus any directory listed in `scanIgnore`) and returns one row per service. Each row prints three flags — `registered` (known to this orchestrator), `installed` (a `node_modules` is resolvable from the service dir upward to the root, so packaging can run without an install) and `packaged` (`.serverless/cloudformation-template-update-stack.json` exists) — followed by the resolved `api:`/`invoke:` ports when known. A second line shows the effective package command for that service (per-service `servicePackaging`, else the global one), and any scan warnings are listed underneath — the "not packaged" one worded from the **effective** `autoPackage`, so it never promises packaging a config with `autoPackage: false` will refuse. Empty results print the root that was searched.
71
73
 
72
74
  **Where it lives.** `bin/cli.js` — `scanServices()` and the row formatting. `src/server/routes/services.ts` — `GET /scan`, which overlays runtime/packaging config on the raw scan. `src/server/services/service-scanner.ts` — the filesystem walk and the installed/packaged detection.
73
75
 
@@ -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 warns and falls back to the search rather than exiting, so `stop`/`status` can never orphan a running instance. Env overrides mirror the server's exactly: `LSS_DASHBOARD_PORT` or `PORT` for `serverPort`, `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.
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
 
@@ -189,7 +202,7 @@ serverless | sls | osls package [args…]
189
202
  npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
190
203
  ```
191
204
 
192
- `npx serverless package` (the default), `npm run package`, `yarn run package`, `sls package -c custom.yml` and `npx -y serverless@3.38.0 package` all pass. `/bin/sh -c …`, `node -e …`, `/usr/bin/env`, `./tool`, `npm exec -c …`, `npx -c …`, `yarn dlx …` and a bare `yarn package` (yarn 1's implicit `run`, which is what makes `yarn node -e` indistinguishable from a script name) are 400s. On top of the grammar, every token of the command **and every `packageArgs` element** is screened for flags that re-point an allowed program somewhere else — `--node-options`, `--script-shell`, `--shell`, `--call`, `--userconfig`, `--globalconfig`, `--use-yarnrc`, `--registry` (npx fetches the CLI when the service has no local copy, and npm reads its config flags from anywhere in the argv), `--prefix`, `--cwd`, `--dir` and node's own `--require`/`--eval`/`--print`/`--import`/`--loader`/`--input-type` — compared with dashes and underscores stripped, so `--nodeOptions` is the same flag. Serverless's short flags (`-c`, `-p`, `-r` = `--config`, `--package`, `--region`) are deliberately *not* screened: the grammar already makes them unreachable as npm/npx options. `packageEnv` likewise refuses keys the runtime or dynamic linker reads before `main()` — `NODE_OPTIONS`, `NODE_REPL_EXTERNAL_MODULE`, `LD_PRELOAD`, `LD_AUDIT`, `LD_LIBRARY_PATH`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, compared case-insensitively — since `{"NODE_OPTIONS":"--require /tmp/x.js"}` turns an otherwise blameless `npx serverless package` into an arbitrary-code runner whichever runner passed. Two limits worth stating out loud: the fence is on the API and **not** the read path (a hand-edited `lss.config.json` and `LSS_PACKAGE_COMMAND` are the operator's own shell, and anyone who can write either already runs code here), and what stays expressible is the project's own build — `npm run <script>` runs whatever the service's `package.json` declares and `serverless package` reads its `serverless.yml`, plugins included. That is code the operator already trusts by pointing LSS at the directory, and forbidding it would mean the dashboard could not set a package command at all, which is what onboarding is built on. What is gone is the part that made the other allowlists worthless: through this API a caller can no longer choose the program, only ask the project's own build to run.
205
+ `npx serverless package` (the default), `npm run package`, `yarn run package`, `sls package -c custom.yml` and `npx -y serverless@3.38.0 package` all pass. `/bin/sh -c …`, `node -e …`, `/usr/bin/env`, `./tool`, `npm exec -c …`, `npx -c …`, `yarn dlx …` and a bare `yarn package` (yarn 1's implicit `run`, which is what makes `yarn node -e` indistinguishable from a script name) are 400s. On top of the grammar, every token of the command **and every `packageArgs` element** is screened for flags that re-point an allowed program somewhere else — `--node-options`, `--script-shell`, `--shell`, `--call`, `--userconfig`, `--globalconfig`, `--use-yarnrc`, `--registry` (npx fetches the CLI when the service has no local copy, and npm reads its config flags from anywhere in the argv), `--prefix`, `--cwd`, `--dir` and node's own `--require`/`--eval`/`--print`/`--import`/`--loader`/`--input-type` — compared with dashes and underscores stripped, so `--nodeOptions` is the same flag. Serverless's short flags (`-c`, `-p`, `-r` = `--config`, `--package`, `--region`) are deliberately *not* screened: the grammar already makes them unreachable as npm/npx options. `packageEnv` likewise refuses keys the runtime or dynamic linker reads before `main()` — `NODE_OPTIONS`, `NODE_REPL_EXTERNAL_MODULE`, `LD_PRELOAD`, `LD_AUDIT`, `LD_LIBRARY_PATH`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, plus `PATH` and `NODE_PATH` (the child's `PATH` is what `execvp(3)` uses to pick which file named `npm` runs, so setting it names an arbitrary binary), compared case-insensitively — since `{"NODE_OPTIONS":"--require /tmp/x.js"}` turns an otherwise blameless `npx serverless package` into an arbitrary-code runner whichever runner passed. Two limits worth stating out loud: the fence is on the API and **not** the read path (a hand-edited `lss.config.json` and `LSS_PACKAGE_COMMAND` are the operator's own shell, and anyone who can write either already runs code here), and what stays expressible is the project's own build — `npm run <script>` runs whatever the service's `package.json` declares and `serverless package` reads its `serverless.yml`, plugins included. That is code the operator already trusts by pointing LSS at the directory, and forbidding it would mean the dashboard could not set a package command at all, which is what onboarding is built on. What is gone is the part that made the other allowlists worthless: through this API a caller can no longer choose the program, only ask the project's own build to run.
193
206
 
194
207
  **Where it lives.** `src/server/services/serverless-packager.ts` — quote-aware command parsing, spawn, timeout, `ServerlessPackageError`. `src/server/services/service-registrar.ts` — `readTemplate()` fallback and `reregister()`. `src/server/services/config-manager.ts` — `getPackageConfigForService()` merges global config with a `servicePackaging` entry. `src/server/routes/services.ts` — `POST /package`.
195
208
 
@@ -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 dropped silently and the Serverless Framework's `ServerlessDeploymentBucket` is dropped by logical id; `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`.
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 any route declares CORS; a handler failure returns 500 for HTTP APIs and 502 for REST.
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 — any `Allow` statement authorizes, otherwise **403** — and a failed or malformed authorizer is **500**. Decisions are cached per `(service, authorizer, identity values)` for `resultTtlInSeconds` when that is greater than 0. The resulting `principalId`/`context` is injected as `requestContext.authorizer` (values stringified for v1) or `requestContext.authorizer.lambda` for v2.
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. TTL is lazy — expired items are invisible and physically removed on read, emitting a `REMOVE` stream record with `userIdentity: {type: "Service", principalId: "dynamodb.amazonaws.com"}`. 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.
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 once its receive count exceeds `maxReceiveCount`, preserving `MessageId` and body; an unresolvable target logs one warning and disables redrive. 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.
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 straight back. 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` naming the service and operation and pointing at `docs/SELF_ENGINE.md#coverage` — 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.
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
 
@@ -734,18 +769,23 @@ npx [-y|--yes]… serverless|sls|osls[@version] package [args…]
734
769
  **How it works.** The Overview polls `GET /api/lambdas/activity` every 5 s for a window you pick
735
770
  (1/2/10 min). The server answers from a stack-wide ring of invocation spans (capped at 1000,
736
771
  log-free) plus the current worker table and host counters. Three readings, coarse to fine: stat
737
- tiles (resident workers against the `maxWarmWorkers` ceiling, peak parallelism, host memory %, load
738
- normalised by core count), a step area of parallelism over the window, and a timeline with one row
739
- per service and one bar per invocation — overlapping bars in a vertical slice *are* the parallelism.
772
+ tiles (resident workers against the `maxWarmWorkers` ceiling, **lambdas loaded against lambdas
773
+ declared**, peak parallelism, host memory %, load normalised by core count), a step area of
774
+ parallelism over the window, and a timeline with one row **per lambda** and one bar per invocation —
775
+ overlapping bars in a vertical slice *are* the parallelism. Under it, each live worker lists the
776
+ handlers actually inside it: a worker is one process serving every function of its service, each
777
+ loaded on its own first invocation, so "the service is warm" answers a different question from
778
+ "which lambda is up" — with 1024 registered functions and 20 in use, the second one is the question.
740
779
  Parallelism is the **peak** per bucket, never an average: a 300 ms burst that saturates the host is
741
780
  the thing you are looking for, and an average erases it. A span still running when the window opens
742
781
  is counted, so a long handler shows as load rather than as silence.
743
782
 
744
- **Where it lives.** `src/ui/src/components/ActivityPanel.vue` — the panel and its hand-rolled SVG
745
- (a charting gap tracked as TREEUX-003 in `src/ui/treeUxPatterns.md`).
783
+ **Where it lives.** `src/ui/src/components/ActivityPanel.vue` — the panel.
746
784
  `src/server/services/invocation-activity.ts` — the ring, the bucketing and the totals.
747
785
  `src/server/routes/lambdas.ts` — `GET /activity`, which joins it to the worker table and host
748
- counters. `src/server/services/lambda-runtime-manager.ts` — records one span per invocation.
786
+ counters. `src/server/services/lambda-runtime-manager.ts` — records one span per invocation and
787
+ mirrors each worker's loaded-handler set. `src/server/runtime/runtime-worker.ts` — reports the
788
+ handler it loaded with every result, since the module cache is the worker's own state.
749
789
 
750
790
  **Why it exists.** Lazy workers and an idle unload make the stack cheap, but they also make it
751
791
  opaque: "is anything resident right now, and is my machine slow because of LSS?" had no answer short
@@ -821,6 +861,17 @@ red/green are 4.4 ΔE apart under deuteranopia.
821
861
 
822
862
  </details>
823
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
+
824
875
  <details>
825
876
  <summary><b>Lambdas tab</b> — list every function and invoke it with a JSON payload from the browser</summary>
826
877
 
@@ -934,7 +985,7 @@ red/green are 4.4 ΔE apart under deuteranopia.
934
985
  <details>
935
986
  <summary><b>Trilingual UI</b> — the whole dashboard in English, Brazilian Portuguese or Spanish</summary>
936
987
 
937
- **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 sets `document.documentElement.lang`. 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.
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.
938
989
 
939
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).
940
991
 
@@ -956,13 +1007,13 @@ red/green are 4.4 ΔE apart under deuteranopia.
956
1007
  </details>
957
1008
 
958
1009
  <details>
959
- <summary><b>MCP server (`lss mcp`) — 25 tools</b> — hands an AI coding agent read and write access to the running stack.</summary>
1010
+ <summary><b>MCP server (`lss mcp`) — 26 tools</b> — hands an AI coding agent read and write access to the running stack.</summary>
960
1011
 
961
- **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 25 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.
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.
962
1013
 
963
- **Where it lives.** `src/mcp/tools.ts` — the 25 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`.
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`.
964
1015
 
965
- **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`.
966
1017
 
967
1018
  </details>
968
1019
 
@@ -1015,7 +1066,7 @@ red/green are 4.4 ΔE apart under deuteranopia.
1015
1066
  <details>
1016
1067
  <summary><b>Edit the config from the dashboard</b> — `PUT /api/config` writes the file and tells you what needs a restart</summary>
1017
1068
 
1018
- **How it works.** `PUT /api/config` takes a partial patch and validates every key against a spec table (`port` = integer 1–65535, `positiveInt`, `nonNegativeInt`, `boolean`, `string` with optional enum, `packageCommand`, `packageArgs`, `stringRecord`, `envRecord`, `object` with fixed subkeys or per-entry subkeys); unknown keys and unknown subkeys are rejected, and **all** errors come back at once as a 400 with a `details[]` array. Three keys get more than a type check because they become arguments to `spawn()` and this route is unauthenticated: `packageCommand` (global and every `servicePackaging[*]` override) must match the packaging grammar — runner **and** subcommand, so `npm exec -c '<shell>'` and `yarn dlx <pkg>` are rejected alongside `/bin/sh`; `packageArgs` is screened for runner-redirecting flags because it reaches the same argv without a tokenizer; and `packageEnv` must carry no code-injection key (`NODE_OPTIONS`, `LD_PRELOAD`, `DYLD_*`, …). `secrets` is explicitly blocked — seed material must never cross the dashboard API. The write re-reads the raw file from disk and patches it key-by-key (never the env-resolved in-memory config, so env values are not baked in); object blocks merge one level deep, map blocks like `serviceRuntime` two levels, and a `null` deletes a key or subkey. The manager then reloads and answers with `restartRequired` — the diff of boot-materialized values (`serverPort`, `enableDynamoProxy`, `dynamoProxyPort`, `region`, `persistence`, `debug`, `stateDir`, and the `selfEngine` block), which keep their old value until `lss stop && lss start` — plus `envOverridden`, the patched keys currently masked by an env var. `POST /api/config/reload` runs the same reload after a hand edit; if the loaded file no longer parses it answers 400 and keeps the working config. `GET /api/config` returns the resolved snapshot with `packageEnv` collapsed to key names and secret seeds to a count, and `GET /api/config/ports` lists every port the stack exposes (orchestrator, engine, DynamoDB proxy, each service's API and invoke listener).
1069
+ **How it works.** `PUT /api/config` takes a partial patch and validates every key against a spec table (`port` = integer 1–65535, `positiveInt`, `nonNegativeInt`, `boolean`, `string` with optional enum, `packageCommand`, `packageArgs`, `stringRecord`, `envRecord`, `object` with fixed subkeys or per-entry subkeys); unknown keys and unknown subkeys are rejected, and **all** errors come back at once as a 400 with a `details[]` array. Three keys get more than a type check because they become arguments to `spawn()` and this route is unauthenticated: `packageCommand` (global and every `servicePackaging[*]` override) must match the packaging grammar — runner **and** subcommand, so `npm exec -c '<shell>'` and `yarn dlx <pkg>` are rejected alongside `/bin/sh`; `packageArgs` is screened for runner-redirecting flags because it reaches the same argv without a tokenizer; and `packageEnv` must carry no code-injection key (`NODE_OPTIONS`, `LD_PRELOAD`, `DYLD_*`, …). `secrets` is explicitly blocked — seed material must never cross the dashboard API. The write re-reads the raw file from disk and patches it key-by-key (never the env-resolved in-memory config, so env values are not baked in); object blocks merge one level deep, map blocks like `serviceRuntime` two levels, and a `null` deletes a key or subkey. **`packageEnv` — global and every `servicePackaging[*]` copy — merges one level deeper still, per variable**: `{"packageEnv":{"A":"v"}}` sets only `A` and leaves `B` and `C` alone, `{"packageEnv":{"A":null}}` removes only `A`, and `{"packageEnv":null}` still drops the whole block (a map left empty by a removal is dropped too, rather than written as `{}`). It is the one editable key whose values are **write-only** — `GET /api/config` reports `packageEnvKeys`, the names alone — so a dashboard that never received `B` and `C` has nothing to resend for them, and a replace-the-map write would have deleted them on the first save. Per-variable is what makes editing packaging env vars from the UI possible at all. The loader-hook denylist is unchanged for every variable a patch **sets**; *removing* one is always allowed, since it only takes capability away and is the one way to undo a hand-edited `NODE_OPTIONS` from the dashboard (the fence guards this API, never the file). The manager then reloads and answers with `restartRequired` — the diff of boot-materialized values (`serverPort`, `enableDynamoProxy`, `dynamoProxyPort`, `region`, `persistence`, `debug`, `stateDir`, and the `selfEngine` block), which keep their old value until `lss stop && lss start` — plus `envOverridden`, the patched keys currently masked by an env var. `POST /api/config/reload` runs the same reload after a hand edit; if the loaded file no longer parses it answers 400 and keeps the working config. `GET /api/config` returns the resolved snapshot with `packageEnv` collapsed to key names and secret seeds to a count, and `GET /api/config/ports` lists every port the stack exposes (orchestrator, engine, DynamoDB proxy, each service's API and invoke listener).
1019
1070
 
1020
1071
  **Where it lives.** `src/server/routes/config.ts` — the four endpoints and the redacted snapshot builder. `src/server/services/config-manager.ts` — `EDITABLE_CONFIG_KEYS`, `updateConfig()`, `reloadFromDisk()`, `resolvedBootValues()`. `src/ui/src/pages/SettingsPage.vue` — the form, which PUTs only the fields actually changed and renders the restart / env-masked badges.
1021
1072
 
@@ -1037,9 +1088,9 @@ red/green are 4.4 ΔE apart under deuteranopia.
1037
1088
  <details>
1038
1089
  <summary><b>Per-service overrides</b> — one service can package or run differently without forking the global settings</summary>
1039
1090
 
1040
- **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.
1041
1092
 
1042
- **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.
1043
1094
 
1044
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.
1045
1096
 
@@ -1072,14 +1123,14 @@ npx lss start
1072
1123
  ```
1073
1124
 
1074
1125
  ```
1075
- AWS_ENDPOINT=http://localhost:14566 # point your services here, done
1126
+ AWS_ENDPOINT_URL=http://localhost:14566 # point your services here, done (LSS-run handlers get it automatically)
1076
1127
  ```
1077
1128
 
1078
1129
  ```mermaid
1079
1130
  flowchart LR
1080
1131
  APP[Your services<br/>AWS SDK → :14566] --> ENG
1081
1132
  subgraph ORCH["One orchestrator process — no Docker"]
1082
- 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]
1083
1134
  ENG -->|in-process events| RT[LSS Lambda runtime<br/>your handlers, ports 30xx/130xx]
1084
1135
  ENG --- FS[(local files<br/>~/.lss/engine)]
1085
1136
  end
@@ -1153,6 +1204,7 @@ npx lss status # is it running? which ports?
1153
1204
  npx lss logs # tail the orchestrator log
1154
1205
  npx lss scan # list Serverless/osls services under the project root
1155
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
1156
1208
  npx lss seed [table] # apply DynamoDB seed files (all tables or one)
1157
1209
  npx lss seed:clear [table] -y # empty seeded tables (prompts unless --yes)
1158
1210
  npx lss stop # stop the orchestrator
@@ -1248,8 +1300,10 @@ Boot prints a warning naming exactly what you opened. Full detail:
1248
1300
 
1249
1301
  The Vue 3 dashboard at `http://localhost:14566` has ten tabs — **Overview** (health,
1250
1302
  config snapshot, exposed-ports map), **Services** (status, start/stop, live logs,
1251
- per-service resource breakdown incl. EventBridge buses & rules), **Lambdas** (registry +
1252
- invoke), **APIs** (emulated HTTP routes), **Queues** (send/receive/purge, consumers),
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),
1253
1307
  **S3** (objects, upload/download), **DynamoDB** (items explorer, editor, TTL, seeds),
1254
1308
  **OpenSearch**, **Secrets**, and **Settings** — which edits `lss.config.json` in place
1255
1309
  (only changed fields are written, so you review and commit the diff), hot-reloads what it