lambder 6.0.1 → 7.0.1

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 (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +26 -24
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
package/README.md CHANGED
@@ -7,16 +7,14 @@ idempotency), so an application is a set of declarations rather than a pile of
7
7
  per-handler boilerplate.
8
8
 
9
9
  ```typescript
10
- import { initLambder, LambderLocalFileSource } from "lambder";
10
+ import { initLambder, LambderLocalFileSource, LambderDdbSessionStore } from "lambder";
11
11
  import { z } from "zod";
12
12
 
13
13
  const lambder = initLambder<SessionData>().create({
14
14
  apiPath: "/api",
15
15
  files: new LambderLocalFileSource({ root: "./public" }),
16
- session: { tableName: "app-session", tableRegion: "us-east-1", sessionSalt: process.env.SESSION_SALT! },
17
- });
18
-
19
- lambder.addApi("getCompany", {
16
+ session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: process.env.SESSION_SALT! },
17
+ }).addApi("getCompany", {
20
18
  input: z.object({ slug: z.string() }),
21
19
  output: z.object({ id: z.string(), name: z.string() }),
22
20
  }, async ({ apiPayload }, res) => res.api(await loadCompany(apiPayload.slug)));
@@ -25,6 +23,10 @@ export type ApiContractType = typeof lambder.ApiContract;
25
23
  export const handler = lambder.getHandler();
26
24
  ```
27
25
 
26
+ Registration chains onto the creation call: every `addApi` returns an instance
27
+ carrying the contract so far, so the whole backend is one declaration and
28
+ `lambder.ApiContract` is the accumulated type.
29
+
28
30
  The frontend imports that contract type and gets autocomplete, typed payloads
29
31
  and typed results with no hand-written client:
30
32
 
@@ -44,8 +46,14 @@ const company = await caller.api("getCompany", { slug: "acme" });
44
46
  and consumed by the frontend as a type-only import.
45
47
  - **Simple route and API declaration.** Paths, regexes, predicates and
46
48
  structured matchers, chained fluently.
47
- - **Sessions.** DynamoDB-backed, with secrets hashed at rest, sliding
48
- expiration, data refresh and cross-subdomain cookies.
49
+ - **Sessions.** Over a store of your choosing (DynamoDB, memory, your own),
50
+ with secrets hashed at rest, sliding expiration, data refresh and
51
+ cross-subdomain cookies.
52
+ - **One API core, two runtimes.** The request pipeline (envelope, refusals,
53
+ sessions, guards, rate limits, idempotency) is one isomorphic class; the
54
+ Lambda server and the mock runtime are adapters over it, so a mock behaves
55
+ like the server by construction and the whole policy layer is testable
56
+ in-process with no AWS.
49
57
  - **Declarative policies.** Named rate-limit policies, authorization guards and
50
58
  idempotency, referenced by name from an API declaration and checked at
51
59
  compile time.
@@ -69,18 +77,19 @@ const company = await caller.api("getCompany", { slug: "acme" });
69
77
  npm install lambder zod
70
78
  ```
71
79
 
72
- `zod` and the AWS SDK clients are optional peer dependencies, so installing
73
- lambder never drags them into your tree. Add whatever the code you actually
74
- import needs:
80
+ `zod` is a required peer dependency: the published declarations name its types,
81
+ so npm installs it alongside lambder. The four AWS SDK clients and `msw` are
82
+ optional peers, so installing lambder never drags them into your tree. Add
83
+ whatever the code you actually import needs:
75
84
 
76
85
  | What you import | What to install alongside |
77
86
  | --- | --- |
78
- | `lambder/client` (browser, shared isomorphic code) | `zod` |
79
- | `lambder` on AWS Lambda (`nodejs18.x` and later) | `zod`. The runtime already provides the AWS SDK v3, so mark the SDK packages as dev dependencies and keep them out of the deployment package |
87
+ | `lambder/client` (browser, shared isomorphic code) | `zod`. `LambderCookieJar` pulls in `tough-cookie` and its public suffix list, so a bundle that never imports the jar never carries either |
88
+ | `lambder` on AWS Lambda (any current Node.js runtime; the package needs Node 20 or later) | `zod`. The runtime already provides the AWS SDK v3, so mark the SDK packages as dev dependencies and keep them out of the deployment package |
80
89
  | `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` when sessions or the DynamoDB stores are used; both are loaded on the first table access, so an app that uses neither needs neither |
81
90
  | `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
82
91
  | `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
83
- | `lambder/testing` | `msw` |
92
+ | `lambder/mock` | nothing; `msw` only for the optional network-panel adapter |
84
93
 
85
94
  The SDK and its `@smithy` tree are roughly 21MB installed, which is why they are
86
95
  peers rather than dependencies: a frontend importing only `lambder/client` has
@@ -94,19 +103,28 @@ The package ships three entry points; pick by where the code runs:
94
103
 
95
104
  | Entry | Runs in | Carries |
96
105
  | --- | --- | --- |
97
- | `lambder` | Server (Lambda) | The full framework: pipeline, sessions, DDB stores, policies, plus everything from `lambder/client` |
98
- | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiError`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
99
- | `lambder/testing` | Dev and test tooling | `LambderMSW`, the MSW adapter that serves your typed contract from mock handlers |
106
+ | `lambder` | Server (Lambda) | The full framework: pipeline, sessions, DDB stores, policies, plus the API core's building blocks and everything from `lambder/client` except the two request-compression helpers, `compressPayloadGzip` and `isRequestCompressionAvailable`, which stay on the client entry where a payload is compressed |
107
+ | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
108
+ | `lambder/mock` | Browser and Node, in development and tests | `LambderMockApp`, the mock runtime: your typed contract served from mock handlers over the real API pipeline and memory stores |
100
109
 
101
110
  Frontends and shared isomorphic packages should import from `lambder/client`
102
111
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
103
112
  pipeline, so the browser boundary is structural rather than left to
104
113
  tree-shaking.
105
114
 
106
- Source layout mirrors this: `src/core/` (request pipeline), `src/policies/`
107
- (declarative rate limits, guards, idempotency), `src/session/`, `src/stores/`
108
- (DynamoDB primitives), `src/client/`, `src/invoke/` (the lambda-to-lambda
109
- caller), and `src/shared/` (isomorphic modules both entries re-export).
115
+ Source layout mirrors this: `src/api/` (the isomorphic API core: request,
116
+ answer, envelope, pipeline, and the declarative policies the pipeline runs),
117
+ `src/core/` (the Lambda server adapter: routes, files, hooks, finalization),
118
+ `src/session/` (the session manager, controller and crypto), `src/stores/`
119
+ (every store implementation, DynamoDB and in-memory alike), `src/client/`,
120
+ `src/invoke/` (the lambda-to-lambda caller and the in-process handler
121
+ transport), `src/mock/` (the mock runtime), and `src/shared/` (isomorphic
122
+ modules every entry re-exports, grouped into `wire/` for the format both
123
+ sides speak, `contracts/` for the four store interfaces, `transport/` for the
124
+ caller-to-server seam, and `util/` for helpers).
125
+ Directories are layers and imports only ever point down;
126
+ [docs/api-core.md](./docs/api-core.md#layering) states the order and the test
127
+ that enforces it.
110
128
 
111
129
  ## Documentation
112
130
 
@@ -119,16 +137,17 @@ guide that matches what you are building. The full index lives in
119
137
  | [Getting started](./docs/getting-started.md) | The three-step path from a first API to a typed frontend call |
120
138
  | [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
121
139
  | [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, and non-HTTP invocations |
122
- | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiError` |
140
+ | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal` |
123
141
  | [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
124
- | [Sessions](./docs/sessions.md) | DynamoDB sessions, cookie scope, secrets at rest, `dataRefresh`, the controller API |
142
+ | [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
125
143
  | [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
126
144
  | [Calling another lambda](./docs/invoke.md) | `LambderInvokeCaller`: invoking a Lambder app in another function, its contract, failures and compression |
127
- | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression |
145
+ | [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
146
+ | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
128
147
  | [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
129
148
  | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
130
149
  | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, runtime dictionaries |
131
- | [Testing](./docs/testing.md) | `LambderMSW`: typed MSW mocking of the API contract |
150
+ | [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
132
151
  | [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
133
152
  | [Exports reference](./docs/exports.md) | Every name the three entry points export, grouped by purpose |
134
153
 
@@ -142,22 +161,30 @@ framework:
142
161
  | `html` / `xml` tags + `LambderTemplatingEngine` | [Templating](./docs/templating.md) | Type-safe tagged templates and a comment-only HTML template engine (build-pipeline-safe) |
143
162
  | `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension and auto language detection (isomorphic) |
144
163
  | `LambderDdbCache` | [DynamoDB cache](./docs/ddb-cache.md) | DynamoDB-backed compressed JSON cache with lease-based single-fill and grouped keys (server-only) |
145
- | `LambderDdbRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | DynamoDB fixed-window rate limiter, atomic per window (server-only) |
146
- | `LambderDdbIdempotency` | [Idempotency store](./docs/ddb-idempotency.md) | DynamoDB idempotency records with owner-checked claims and compressed replays (server-only) |
147
- | `LambderMSW` | [Testing](./docs/testing.md) | Typed MSW mocking of the API contract for frontend development |
164
+ | `LambderDdbRateLimiter` / `LambderMemoryRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | Fixed-window rate limiter, atomic per window, in DynamoDB (server-only) or in memory |
165
+ | `LambderDdbIdempotencyStore` / `LambderMemoryIdempotencyStore` | [Idempotency store](./docs/ddb-idempotency.md) | Idempotency records with owner-checked claims, in DynamoDB (compressed replays, server-only) or in memory |
166
+ | `LambderMockApp` | [The mock runtime](./docs/mock.md) | The typed contract served from mock handlers over the real API pipeline, with failure injection, sessions and a call log (isomorphic) |
148
167
 
149
168
  ## Versioning and changes
150
169
 
151
170
  Released versions and what each one changed are in
152
- [CHANGELOG.md](./CHANGELOG.md). The current major is v5, which is v4's API
153
- plus this documentation set: upgrading from 4.x needs no code changes.
154
- Upgrading from 3.x is covered by the breaking-changes section of the 4.0.1
155
- entry.
171
+ [CHANGELOG.md](./CHANGELOG.md). The current major is v7, which moved the API
172
+ pipeline into an isomorphic core, put the session layer behind a store
173
+ interface, dropped the resolver argument from guards, and replaced the MSW
174
+ adapter with a mock runtime. Every break and what to do about it is in the
175
+ 7.0.0 entry. The compiler finds most of them. Three it cannot are named there:
176
+ leftover `session` fields that `const` generics stop it from seeing, a
177
+ `region` that went from required to optional, and mock handlers that now take
178
+ the call context rather than the payload.
156
179
 
157
180
  ## Contributing
158
181
 
159
- Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to
160
- run the tests and what a good change looks like.
182
+ Contributions are welcome. Open an issue for a bug or an idea, or send a pull
183
+ request. `npm test` typechecks, builds `dist/` and runs the suite, and it must
184
+ pass before a change is ready: the type system carries a lot of this
185
+ framework's guarantees, so a change that only passes at runtime is not
186
+ finished. New behavior belongs in the matching page under
187
+ [docs/](./docs/README.md) and in [CHANGELOG.md](./CHANGELOG.md) too.
161
188
 
162
189
  ## License
163
190
 
@@ -0,0 +1,40 @@
1
+ import type { LambderApiHttpAnswer } from "../shared/wire/LambderApiOutcome.js";
2
+ /**
3
+ * The core's output for one API call: what goes back over the wire before
4
+ * any transport-level finalization. The server adapter turns it into a
5
+ * Lambda response (compression, ETag, base64); the mock transport hands it
6
+ * to the caller as it is. It is also the shape the idempotency store
7
+ * persists and replays, which is why it is plain data.
8
+ *
9
+ * Header names keep the casing they were written with; lookups are
10
+ * case-insensitive (see getAnswerHeader), the way LambderResponse treats
11
+ * them.
12
+ */
13
+ export type LambderApiAnswer = {
14
+ statusCode: number;
15
+ headers: Record<string, string[]>;
16
+ body: string;
17
+ /**
18
+ * Finalization hints for the server adapter, carried through so an
19
+ * answer that started as a LambderResponse loses nothing on the way:
20
+ * `isBodyBase64` marks a body that is base64 of binary bytes (never
21
+ * cached by the idempotency engine, never compressed), `compress` and
22
+ * `etag` are the LambderResponse flags. The mock runtime finalizes
23
+ * nothing, so it hands the body to the caller as it stands; the stores
24
+ * drop the hints.
25
+ */
26
+ isBodyBase64?: boolean;
27
+ compress?: boolean | "auto";
28
+ etag?: boolean | "auto";
29
+ };
30
+ /**
31
+ * An answer in the accessor form resolveApiOutcome() reads, the same view a
32
+ * fetch Response or a decoded Lambda result is given. What the mock
33
+ * transport hands the caller.
34
+ *
35
+ * The body is handed over as it stands, base64 hint or not: the only answers
36
+ * that reach here are the mock runtime's own, which are JSON envelopes, and
37
+ * a caller reading a binary body through the JSON accessors would have
38
+ * nothing to do with what it decoded anyway.
39
+ */
40
+ export declare const toHttpAnswer: (answer: LambderApiAnswer) => LambderApiHttpAnswer;
@@ -0,0 +1,19 @@
1
+ import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
2
+ /**
3
+ * An answer in the accessor form resolveApiOutcome() reads, the same view a
4
+ * fetch Response or a decoded Lambda result is given. What the mock
5
+ * transport hands the caller.
6
+ *
7
+ * The body is handed over as it stands, base64 hint or not: the only answers
8
+ * that reach here are the mock runtime's own, which are JSON envelopes, and
9
+ * a caller reading a binary body through the JSON accessors would have
10
+ * nothing to do with what it decoded anyway.
11
+ */
12
+ export const toHttpAnswer = (answer) => ({
13
+ status: answer.statusCode,
14
+ statusText: "",
15
+ header: (name) => getAnswerHeader(answer.headers, name)?.join(", ") ?? null,
16
+ json: async () => JSON.parse(answer.body),
17
+ text: async () => answer.body,
18
+ setCookies: getAnswerHeader(answer.headers, "Set-Cookie") ?? [],
19
+ });
@@ -0,0 +1,38 @@
1
+ import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
2
+ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
3
+ /**
4
+ * The context the API core needs from whoever runs it. The server's render
5
+ * context and the mock runtime's handler context both extend it; the
6
+ * pipeline, the policy engines and the session controller read and write
7
+ * nothing else on a context, so they never learn which adapter they run
8
+ * under.
9
+ *
10
+ * - `session` is set by the pipeline on session APIs (and by the session
11
+ * controller when a handler creates or ends one).
12
+ * - `guardData` receives the return values of the guards that ran.
13
+ * - `responseHeaders` collects headers written during the call, applied
14
+ * onto the answer by the pipeline.
15
+ * - `logList` collects entries for the envelope's logList channel
16
+ * (`res.logToApiResponse` on the server).
17
+ */
18
+ export type LambderApiCallContext<TSessionData = any> = {
19
+ session: LambderSessionRecord<TSessionData> | null;
20
+ guardData: Record<string, unknown>;
21
+ responseHeaders: LambderAnswerHeaders;
22
+ logList: unknown[];
23
+ };
24
+ /** A fresh call context: no session, no guard data, nothing pending. */
25
+ export declare const createApiCallContext: <TSessionData = any>() => LambderApiCallContext<TSessionData>;
26
+ /**
27
+ * What one call recorded about itself while it ran, in the order things
28
+ * happened. Written as the call goes rather than assembled from what each
29
+ * step returned, so a refusal partway through still reports the guards that
30
+ * had already run: the mock's call log shows exactly the calls a developer is
31
+ * looking at when something denied them.
32
+ */
33
+ export type LambderApiCallTrace = {
34
+ /** The guards that ran, in order, including on a call that a later one refused. */
35
+ guardsRun: string[];
36
+ /** True when a stored idempotent answer was replayed and no handler ran. */
37
+ replayed: boolean;
38
+ };
@@ -0,0 +1,13 @@
1
+ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
2
+ /** A fresh call context: no session, no guard data, nothing pending. */
3
+ export const createApiCallContext = () => ({
4
+ session: null,
5
+ // No prototype, for the reason the guard and policy registries are Maps:
6
+ // guard names are the app's to choose, and on a plain object a guard
7
+ // named "toString" or "constructor" reads back as an inherited function
8
+ // for a handler that only wanted to know whether the guard returned
9
+ // anything.
10
+ guardData: Object.create(null),
11
+ responseHeaders: new LambderAnswerHeaders(),
12
+ logList: [],
13
+ });
@@ -0,0 +1,18 @@
1
+ import type { z } from "zod";
2
+ import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
3
+ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "../shared/wire/LambderApiOptionValues.js";
4
+ /**
5
+ * One endpoint's declaration as the pipeline runs it: what the server's
6
+ * addApi/addSessionApi options carry, minus the handler, in a shape the mock
7
+ * runtime can restate from a type-only contract. The schema is optional
8
+ * because the mock has none; when present, input validation runs and the
9
+ * handler sees the parsed payload.
10
+ */
11
+ export type LambderApiDefinition = {
12
+ name: string;
13
+ mode: LambderApiMode;
14
+ guards?: LambderGuardsOptionValue;
15
+ rateLimit?: LambderRateLimitOptionValue;
16
+ idempotency?: LambderApiIdempotencyOption;
17
+ input?: z.ZodType;
18
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import type { z } from "zod";
2
+ import type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
3
+ import { type LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
4
+ import type { LambderApiAnswer } from "./LambderApiAnswer.js";
5
+ export declare const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
6
+ /** The envelope's config plus the logList channel the call accumulated. */
7
+ export type LambderApiEnvelopeConfig = LambderApiResponseConfig & {
8
+ logList?: unknown[];
9
+ };
10
+ /**
11
+ * The wire envelope for one answer. Flags are only present when set, so a
12
+ * plain success is `{ apiVersion, payload }` and nothing else; an empty
13
+ * logList is omitted.
14
+ */
15
+ export declare const buildApiEnvelope: <T>(apiVersion: string | null | undefined, payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, crash, }?: LambderApiEnvelopeConfig) => LambderApiEnvelopeBody<T>;
16
+ /** An envelope as an answer: JSON body, JSON content type, the status and headers given (200 and none by default). */
17
+ export declare const envelopeAnswer: (envelope: LambderApiEnvelopeBody<unknown>, options?: {
18
+ statusCode?: number;
19
+ headers?: Record<string, string | string[]>;
20
+ }) => LambderApiAnswer;
21
+ /**
22
+ * A thrown refusal as an answer: its errorMessage and flags on the envelope,
23
+ * its status (200 unless it set one) and its extra headers (Retry-After on a
24
+ * rate limit). The logList the call accumulated rides along, as it does on
25
+ * a success.
26
+ */
27
+ export declare const refusalAnswer: (err: LambderApiRefusal, apiVersion: string | null | undefined, logList?: unknown[]) => LambderApiAnswer;
28
+ /** The body a validation refusal carries, the shape resolveApiOutcome reads a 422 by. */
29
+ export type LambderValidationAnswerBody = {
30
+ error: string;
31
+ zodError: {
32
+ name: string;
33
+ message: string;
34
+ issues: z.core.$ZodIssue[];
35
+ };
36
+ /** Present only when the answer was trimmed (issues dropped, or oversized values inside one shortened): how many issues there were. */
37
+ issueCount?: number;
38
+ /** The logList channel the call accumulated, as a success carries it; omitted when empty. */
39
+ logList?: unknown[];
40
+ };
41
+ /**
42
+ * The standard answer for a rejected input: a 422 whose body spells the
43
+ * ZodError out. Spelled out rather than serialized as-is: zod 4 keeps
44
+ * `issues` as a non-enumerable property, so JSON.stringify(zodError) would
45
+ * carry the issues only inside the message string, and a client's
46
+ * validation handler would receive a ZodError with nothing to branch on.
47
+ *
48
+ * zod's own `message` never ships: it is the whole issue tree re-serialized,
49
+ * so carrying it would send every capped byte a second time. The generated
50
+ * summary takes its place on every answer, trimmed or not.
51
+ */
52
+ export declare const validationAnswer: (zodError: z.ZodError, logList?: unknown[]) => LambderApiAnswer;
53
+ /** No API is registered under the requested name: a refusal, not a 404, so a typed caller reads it. */
54
+ export declare const apiNotFoundAnswer: (apiVersion: string | null | undefined, logList?: unknown[]) => LambderApiAnswer;
55
+ /** A session API called without a live session: the protocol's sessionExpired flag, which the caller clears its cookies on. */
56
+ export declare const sessionExpiredAnswer: (apiVersion: string | null | undefined, logList?: unknown[]) => LambderApiAnswer;
57
+ /** The caller's version is behind the server's: the protocol's versionExpired flag, which the caller reloads on. */
58
+ export declare const versionExpiredAnswer: (apiVersion: string | null | undefined) => LambderApiAnswer;
59
+ /** A compressed request payload that could not be restored: a 400 with the reason, never a crash. */
60
+ export declare const invalidPayloadAnswer: (apiVersion: string | null | undefined, message: string) => LambderApiAnswer;
61
+ /**
62
+ * The last-resort answer when the call crashed and nothing else could
63
+ * answer: a 500 that is still an envelope, so a caller reads a structured
64
+ * failure rather than a text page. The server sends it only when its global
65
+ * error handler is absent or itself failed.
66
+ */
67
+ export declare const crashAnswer: (apiVersion: string | null | undefined) => LambderApiAnswer;
@@ -0,0 +1,180 @@
1
+ import { LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
2
+ import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
3
+ /*
4
+ * The one place the API envelope is written, and the one mapping from each
5
+ * kind of protocol outcome onto an answer: a success, a thrown refusal, a
6
+ * rejected input, an unknown name, a missing session, a stale client, a
7
+ * malformed compressed payload, and the last-resort crash. The server's
8
+ * res.api() and the mock runtime's handler wrapping both build through
9
+ * buildApiEnvelope, and the pipeline renders every refusal through the
10
+ * functions below, so the two sides cannot drift on a single byte of the
11
+ * wire format. Pure: no Node built-ins, no response classes.
12
+ */
13
+ export const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
14
+ /**
15
+ * The wire envelope for one answer. Flags are only present when set, so a
16
+ * plain success is `{ apiVersion, payload }` and nothing else; an empty
17
+ * logList is omitted.
18
+ */
19
+ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, crash, } = {}) => ({
20
+ apiVersion: apiVersion ?? null,
21
+ payload,
22
+ ...(versionExpired ? { versionExpired } : {}),
23
+ ...(sessionExpired ? { sessionExpired } : {}),
24
+ ...(notAuthorized ? { notAuthorized } : {}),
25
+ // Presence, not truthiness: the three channels below carry app values,
26
+ // and an app that refuses with errorMessage: "" (or 0, or a message
27
+ // object it built empty) meant to say something. The flags above are
28
+ // booleans, where false and absent are the same statement.
29
+ ...(message !== undefined ? { message } : {}),
30
+ ...(errorMessage !== undefined ? { errorMessage } : {}),
31
+ ...(crash !== undefined ? { crash } : {}),
32
+ ...(logList?.length ? { logList } : {}),
33
+ });
34
+ /** An envelope as an answer: JSON body, JSON content type, the status and headers given (200 and none by default). */
35
+ export const envelopeAnswer = (envelope, options = {}) => {
36
+ const headers = { "Content-Type": [API_ANSWER_CONTENT_TYPE] };
37
+ for (const [key, value] of Object.entries(options.headers ?? {})) {
38
+ // Through setAnswerHeader, so a header the refusal names under another
39
+ // casing replaces the envelope's own rather than shipping beside it.
40
+ setAnswerHeader(headers, key, value);
41
+ }
42
+ return { statusCode: options.statusCode ?? 200, headers, body: JSON.stringify(envelope) };
43
+ };
44
+ /**
45
+ * A thrown refusal as an answer: its errorMessage and flags on the envelope,
46
+ * its status (200 unless it set one) and its extra headers (Retry-After on a
47
+ * rate limit). The logList the call accumulated rides along, as it does on
48
+ * a success.
49
+ */
50
+ export const refusalAnswer = (err, apiVersion, logList) => envelopeAnswer(buildApiEnvelope(apiVersion, null, {
51
+ ...(err.errorMessage !== undefined ? { errorMessage: err.errorMessage } : {}),
52
+ ...(err.notAuthorized ? { notAuthorized: true } : {}),
53
+ ...(err.sessionExpired ? { sessionExpired: true } : {}),
54
+ logList,
55
+ }), {
56
+ ...(err.statusCode !== undefined ? { statusCode: err.statusCode } : {}),
57
+ ...(err.headers ? { headers: err.headers } : {}),
58
+ });
59
+ /**
60
+ * How many issues a 422 spells out before it starts counting instead. A
61
+ * caller fixing their request needs the first few; the rest are the same
62
+ * mistake repeated.
63
+ */
64
+ const MAX_VALIDATION_ISSUES = 50;
65
+ /**
66
+ * What the whole issue list may cost, serialized. The count cap alone bounds
67
+ * the wrong thing: ONE `unrecognized_keys` issue carries every key the client
68
+ * posted, so a strictObject answered a 1MB body with a 4MB one, unauthenticated
69
+ * and before any guard ran, and past maxResponseBytes the 422 became a 500.
70
+ * Bytes are what the amplification is measured in, so bytes are what is
71
+ * capped.
72
+ */
73
+ const MAX_VALIDATION_ISSUES_BYTES = 32_000;
74
+ /** How many entries of one issue's own lists (`keys`, `path`) survive. */
75
+ const MAX_VALIDATION_LIST_ENTRIES = 20;
76
+ /** How long one string inside an issue may be. */
77
+ const MAX_VALIDATION_TEXT_CHARS = 200;
78
+ const utf8Encoder = new TextEncoder();
79
+ const clampText = (value) => value.length > MAX_VALIDATION_TEXT_CHARS ? `${value.slice(0, MAX_VALIDATION_TEXT_CHARS)}...` : value;
80
+ /**
81
+ * One issue with its own strings and lists bounded. Applied field by field
82
+ * rather than to the named fields only, because `keys` is merely the one that
83
+ * grows without a bound TODAY: any issue a schema authors itself may carry a
84
+ * list or a message the client chose the size of.
85
+ */
86
+ const clampIssueValue = (value) => {
87
+ if (typeof value === "string")
88
+ return clampText(value);
89
+ if (Array.isArray(value))
90
+ return value.slice(0, MAX_VALIDATION_LIST_ENTRIES).map(clampIssueValue);
91
+ return value;
92
+ };
93
+ const clampIssue = (issue) =>
94
+ // Structurally an issue with shorter values, so the cast says what the
95
+ // mapping already guarantees: every field is carried through, in kind,
96
+ // and the union's discriminant with it. A mapped object has no way to say
97
+ // that in the type system.
98
+ Object.fromEntries(Object.entries(issue).map(([key, value]) => [key, clampIssueValue(value)]));
99
+ /** The issue list the body may carry: clamped, then cut to the byte budget, whole issues from the end. */
100
+ const boundIssueList = (all) => {
101
+ const issues = [];
102
+ let bytes = 0;
103
+ let trimmed = false;
104
+ for (const issue of all.slice(0, MAX_VALIDATION_ISSUES)) {
105
+ const clamped = clampIssue(issue);
106
+ if (JSON.stringify(clamped) !== JSON.stringify(issue))
107
+ trimmed = true;
108
+ const size = utf8Encoder.encode(JSON.stringify(clamped)).length;
109
+ if (bytes + size > MAX_VALIDATION_ISSUES_BYTES) {
110
+ trimmed = true;
111
+ // A first issue that is over the budget on its own still has to
112
+ // say what it is: the three fields every issue carries, so a
113
+ // client always has a code and a path to branch on.
114
+ if (issues.length === 0) {
115
+ issues.push({ code: clamped.code, path: clamped.path.slice(0, MAX_VALIDATION_LIST_ENTRIES), message: clampText(clamped.message) });
116
+ }
117
+ break;
118
+ }
119
+ issues.push(clamped);
120
+ bytes += size;
121
+ }
122
+ return { issues, trimmed: trimmed || issues.length < all.length };
123
+ };
124
+ /** What the answer says about itself, in place of zod's own message. */
125
+ const summarizeIssues = (total, listed, trimmed) => {
126
+ const head = `${total} validation issue${total === 1 ? "" : "s"}`;
127
+ if (listed < total)
128
+ return `${head}; the first ${listed} are listed.`;
129
+ if (trimmed)
130
+ return `${head}; oversized values are shortened.`;
131
+ return `${head}.`;
132
+ };
133
+ /**
134
+ * The standard answer for a rejected input: a 422 whose body spells the
135
+ * ZodError out. Spelled out rather than serialized as-is: zod 4 keeps
136
+ * `issues` as a non-enumerable property, so JSON.stringify(zodError) would
137
+ * carry the issues only inside the message string, and a client's
138
+ * validation handler would receive a ZodError with nothing to branch on.
139
+ *
140
+ * zod's own `message` never ships: it is the whole issue tree re-serialized,
141
+ * so carrying it would send every capped byte a second time. The generated
142
+ * summary takes its place on every answer, trimmed or not.
143
+ */
144
+ export const validationAnswer = (zodError, logList) => {
145
+ const { issues, trimmed } = boundIssueList(zodError.issues);
146
+ return {
147
+ statusCode: 422,
148
+ headers: { "Content-Type": [API_ANSWER_CONTENT_TYPE] },
149
+ body: JSON.stringify({
150
+ error: "Input validation failed",
151
+ zodError: {
152
+ name: zodError.name,
153
+ message: summarizeIssues(zodError.issues.length, issues.length, trimmed),
154
+ issues,
155
+ },
156
+ ...(trimmed ? { issueCount: zodError.issues.length } : {}),
157
+ ...(logList?.length ? { logList } : {}),
158
+ }),
159
+ };
160
+ };
161
+ /** No API is registered under the requested name: a refusal, not a 404, so a typed caller reads it. */
162
+ export const apiNotFoundAnswer = (apiVersion, logList) => envelopeAnswer(buildApiEnvelope(apiVersion, null, {
163
+ errorMessage: { type: "warning", code: LAMBDER_REFUSAL_CODES.apiNotFound, content: "API not found." },
164
+ logList,
165
+ }));
166
+ /** A session API called without a live session: the protocol's sessionExpired flag, which the caller clears its cookies on. */
167
+ export const sessionExpiredAnswer = (apiVersion, logList) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { sessionExpired: true, logList }));
168
+ /** The caller's version is behind the server's: the protocol's versionExpired flag, which the caller reloads on. */
169
+ export const versionExpiredAnswer = (apiVersion) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { versionExpired: true }));
170
+ /** A compressed request payload that could not be restored: a 400 with the reason, never a crash. */
171
+ export const invalidPayloadAnswer = (apiVersion, message) => envelopeAnswer(buildApiEnvelope(apiVersion, null, {
172
+ errorMessage: { type: "error", code: LAMBDER_REFUSAL_CODES.invalidRequestPayload, content: message },
173
+ }), { statusCode: 400 });
174
+ /**
175
+ * The last-resort answer when the call crashed and nothing else could
176
+ * answer: a 500 that is still an envelope, so a caller reads a structured
177
+ * failure rather than a text page. The server sends it only when its global
178
+ * error handler is absent or itself failed.
179
+ */
180
+ export const crashAnswer = (apiVersion) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { errorMessage: "Internal server error." }), { statusCode: 500 });