lambder 7.3.1 → 8.1.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 (237) hide show
  1. package/CHANGELOG.md +1047 -3
  2. package/README.md +46 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/ContractTypePrinter.d.ts +85 -0
  27. package/dist/build/ContractTypePrinter.js +402 -0
  28. package/dist/build/freshProcessVerifier.d.ts +13 -0
  29. package/dist/build/freshProcessVerifier.js +19 -0
  30. package/dist/build/moduleLocation.d.ts +11 -0
  31. package/dist/build/moduleLocation.js +6 -0
  32. package/dist/build/writeApiContract.d.ts +78 -0
  33. package/dist/build/writeApiContract.js +302 -0
  34. package/dist/build/writeApiSignatures.d.ts +114 -0
  35. package/dist/build/writeApiSignatures.js +217 -0
  36. package/dist/build/writeFileAtomically.d.ts +8 -0
  37. package/dist/build/writeFileAtomically.js +22 -0
  38. package/dist/build.d.ts +14 -0
  39. package/dist/build.js +11 -0
  40. package/dist/client/LambderCaller.d.ts +13 -44
  41. package/dist/client/LambderCaller.js +77 -84
  42. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  43. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  44. package/dist/client/LambderUploadRunner.d.ts +96 -0
  45. package/dist/client/LambderUploadRunner.js +234 -0
  46. package/dist/client/lambderFetchTransport.d.ts +4 -1
  47. package/dist/client/lambderFetchTransport.js +52 -28
  48. package/dist/client.d.ts +9 -3
  49. package/dist/client.js +6 -1
  50. package/dist/core/Lambder.d.ts +143 -79
  51. package/dist/core/Lambder.js +350 -231
  52. package/dist/core/LambderContext.d.ts +82 -15
  53. package/dist/core/LambderContext.js +107 -20
  54. package/dist/core/LambderCors.d.ts +21 -3
  55. package/dist/core/LambderCors.js +35 -16
  56. package/dist/core/LambderCrashHandling.d.ts +40 -0
  57. package/dist/core/LambderCrashHandling.js +97 -0
  58. package/dist/core/LambderCreateOptions.d.ts +151 -75
  59. package/dist/core/LambderCreateOptions.js +16 -23
  60. package/dist/core/LambderFiles.d.ts +21 -7
  61. package/dist/core/LambderFiles.js +62 -34
  62. package/dist/core/LambderIndexHtml.js +12 -11
  63. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  64. package/dist/core/LambderPolicyBuilders.js +17 -5
  65. package/dist/core/LambderPublicFiles.d.ts +11 -5
  66. package/dist/core/LambderPublicFiles.js +32 -4
  67. package/dist/core/LambderRequestPath.d.ts +43 -0
  68. package/dist/core/LambderRequestPath.js +63 -0
  69. package/dist/core/LambderResponse.d.ts +26 -5
  70. package/dist/core/LambderResponse.js +157 -70
  71. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  72. package/dist/core/LambderResponseBuilder.js +64 -3
  73. package/dist/core/LambderRouting.d.ts +2 -3
  74. package/dist/core/LambderRouting.js +22 -7
  75. package/dist/core/LambderTemplatingEngine.js +211 -32
  76. package/dist/index.d.ts +25 -8
  77. package/dist/index.js +13 -4
  78. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  79. package/dist/invoke/LambderInvokeCaller.js +76 -66
  80. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  81. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  82. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  83. package/dist/invoke/LambderLambdaEvent.js +40 -22
  84. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  85. package/dist/invoke/lambderHandlerTransport.js +15 -18
  86. package/dist/mock/LambderMockApp.d.ts +67 -83
  87. package/dist/mock/LambderMockApp.js +167 -153
  88. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  89. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  90. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  91. package/dist/mock/LambderMockCallRecorder.js +19 -28
  92. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  93. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  94. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  95. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  96. package/dist/mock/LambderMockFailureInjector.js +3 -6
  97. package/dist/mock/LambderMockTypes.d.ts +78 -108
  98. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  99. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  100. package/dist/mock/lambderMockMswHandler.d.ts +43 -33
  101. package/dist/mock/lambderMockMswHandler.js +50 -39
  102. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  103. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  104. package/dist/mock.d.ts +4 -1
  105. package/dist/mock.js +6 -3
  106. package/dist/session/LambderSessionController.d.ts +108 -89
  107. package/dist/session/LambderSessionController.js +187 -168
  108. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  109. package/dist/session/LambderSessionCrypto.js +26 -12
  110. package/dist/session/LambderSessionManager.d.ts +124 -46
  111. package/dist/session/LambderSessionManager.js +262 -137
  112. package/dist/shared/LambderHtml.d.ts +42 -3
  113. package/dist/shared/LambderHtml.js +127 -7
  114. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  115. package/dist/shared/LambderHtmlPositions.js +652 -0
  116. package/dist/shared/LambderI18n.d.ts +10 -11
  117. package/dist/shared/LambderI18n.js +33 -21
  118. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  119. package/dist/shared/contracts/LambderCache.js +11 -0
  120. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  121. package/dist/shared/contracts/LambderFileSource.js +5 -8
  122. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  123. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  124. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  125. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  126. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  127. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  128. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  129. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  130. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  131. package/dist/shared/transport/LambderApiTransport.js +7 -7
  132. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  133. package/dist/shared/transport/LambderCookieJar.js +54 -66
  134. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  135. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  136. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  137. package/dist/shared/util/LambderCallAbort.js +5 -5
  138. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  139. package/dist/shared/util/LambderClientIp.js +96 -13
  140. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  141. package/dist/shared/util/LambderContentDisposition.js +13 -0
  142. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  143. package/dist/shared/util/LambderExpiringMap.js +41 -57
  144. package/dist/shared/util/LambderNodeModules.js +6 -7
  145. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  146. package/dist/shared/util/LambderOptionChecks.js +4 -4
  147. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  148. package/dist/shared/util/LambderResponseBrand.js +5 -5
  149. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  150. package/dist/shared/util/LambderTextDigest.js +11 -5
  151. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  152. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  153. package/dist/shared/util/boundKeyField.d.ts +20 -0
  154. package/dist/shared/util/boundKeyField.js +34 -0
  155. package/dist/shared/util/canonicalJson.d.ts +11 -0
  156. package/dist/shared/util/canonicalJson.js +28 -0
  157. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  158. package/dist/shared/util/joinKeyFields.js +22 -0
  159. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  160. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  161. package/dist/shared/wire/LambderApiContract.d.ts +98 -53
  162. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  163. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  164. package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
  165. package/dist/shared/wire/LambderApiRefusal.js +42 -7
  166. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  167. package/dist/shared/wire/LambderApiSignature.js +16 -19
  168. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  169. package/dist/shared/wire/LambderCallOptions.js +9 -11
  170. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  171. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  172. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  173. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  174. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  175. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  176. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  177. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  178. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  179. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  180. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  181. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  182. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  183. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  184. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  185. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  186. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  187. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  188. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  189. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  190. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  191. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  192. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  193. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  194. package/dist/stores/LambderCacheFiller.js +119 -0
  195. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  196. package/dist/stores/LambderCacheKeys.js +54 -0
  197. package/dist/stores/LambderCacheValues.d.ts +45 -0
  198. package/dist/stores/LambderCacheValues.js +74 -0
  199. package/dist/stores/LambderDdbCache.d.ts +121 -56
  200. package/dist/stores/LambderDdbCache.js +528 -225
  201. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  202. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  203. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  204. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  205. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  206. package/dist/stores/LambderDdbSdk.js +80 -38
  207. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  208. package/dist/stores/LambderDdbSessionStore.js +119 -47
  209. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  210. package/dist/stores/LambderHttpFileSource.js +15 -13
  211. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  212. package/dist/stores/LambderMemoryCache.js +113 -0
  213. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  214. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  215. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  216. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  217. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  218. package/dist/stores/LambderMemorySessionStore.js +38 -19
  219. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  220. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  221. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  222. package/dist/stores/LambderS3FileSource.js +12 -7
  223. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  224. package/dist/stores/LambderS3UploadBucket.js +144 -0
  225. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  226. package/dist/stores/LambderSdkInstallHint.js +14 -0
  227. package/dist/testing/LambderTestApp.d.ts +23 -25
  228. package/dist/testing/LambderTestApp.js +22 -24
  229. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  230. package/dist/testing/LambderTestVisitor.js +15 -15
  231. package/dist/testing.d.ts +3 -0
  232. package/dist/testing.js +2 -0
  233. package/package.json +26 -3
  234. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  235. package/dist/api/LambderApiPolicyEngine.js +0 -85
  236. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  237. package/dist/shared/util/LambderKeyFields.js +0 -34
package/README.md CHANGED
@@ -34,7 +34,7 @@ and typed results with no hand-written client:
34
34
  import { LambderCaller } from "lambder/client";
35
35
  import type { ApiContractType } from "./backend/handler";
36
36
 
37
- const caller = new LambderCaller<ApiContractType>({ apiPath: "/api", isCorsEnabled: false });
37
+ const caller = new LambderCaller<ApiContractType>({ apiPath: "/api" });
38
38
  const company = await caller.api("getCompany", { slug: "acme" });
39
39
  ```
40
40
 
@@ -67,6 +67,10 @@ const company = await caller.api("getCompany", { slug: "acme" });
67
67
  - **Frontend hosting.** Serve a build from a folder, S3, R2 or any HTTP
68
68
  origin, with an app shell rendered through a build-pipeline-safe template
69
69
  engine.
70
+ - **Direct uploads.** Files go from the browser straight to S3 on tickets
71
+ that pin their size, type and SHA-256, with a browser runner that hashes,
72
+ retries and reports progress, and a memory bucket that holds tests and the
73
+ mock to the same rules.
70
74
  - **Runs anywhere Lambda does.** API Gateway REST APIs (payload v1), HTTP APIs
71
75
  (payload v2) and Lambda Function URLs; the payload format is detected per
72
76
  event.
@@ -85,7 +89,7 @@ whatever the code you actually import needs:
85
89
  | What you import | What to install alongside |
86
90
  | --- | --- |
87
91
  | `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 |
92
+ | `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, as long as the runtime's `@aws-sdk/client-dynamodb` is new enough for `LambderDdbRateLimiter` (3.868.0, see below) |
89
93
  | `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 |
90
94
  | `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
91
95
  | `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
@@ -95,11 +99,16 @@ The SDK and its `@smithy` tree are roughly 21MB installed, which is why they are
95
99
  peers rather than dependencies: a frontend importing only `lambder/client` has
96
100
  no use for any of it, and a Lambda deployment package should not ship a second
97
101
  copy of what the runtime already loads. The runtime pins its own SDK version,
98
- so if you need a specific one, install it and bundle it yourself.
102
+ so if you need a specific one, install it and bundle it yourself. One version
103
+ matters to Lambder: `LambderDdbRateLimiter` needs `@aws-sdk/client-dynamodb`
104
+ 3.868.0 or later, the first whose throttling errors name their reasons. Under
105
+ an older client it never recognizes a key-range throttle, so a flood on one
106
+ key goes to `failOpen` instead of being refused. Check the version your
107
+ runtime bundles before relying on it, or bundle the client yourself.
99
108
 
100
109
  ## Package entry points
101
110
 
102
- The package ships four entry points; pick by where the code runs:
111
+ The package ships five entry points; pick by where the code runs:
103
112
 
104
113
  | Entry | Runs in | Carries |
105
114
  | --- | --- | --- |
@@ -107,6 +116,7 @@ The package ships four entry points; pick by where the code runs:
107
116
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
108
117
  | `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 |
109
118
  | `lambder/testing` | Node, in tests | `lambderTestApp`: your real instance under test in this process, memory stores put under it in place, simulated browsers with typed callers in front of it, and the outcome assertions |
119
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiContract`: the contract as plain types a client compiles instead of the server |
110
120
 
111
121
  Frontends and shared isomorphic packages should import from `lambder/client`
112
122
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -117,12 +127,15 @@ Source layout mirrors this: `src/api/` (the isomorphic API core: request,
117
127
  answer, envelope, pipeline, and the declarative policies the pipeline runs),
118
128
  `src/core/` (the Lambda server adapter: routes, files, hooks, finalization),
119
129
  `src/session/` (the session manager, controller and crypto), `src/stores/`
120
- (every store implementation, DynamoDB and in-memory alike), `src/client/`,
130
+ (every store and file-source implementation, DynamoDB and in-memory alike,
131
+ and the helpers the two caches share), `src/client/`,
121
132
  `src/invoke/` (the lambda-to-lambda caller and the in-process handler
122
- transport), `src/mock/` (the mock runtime), and `src/shared/` (isomorphic
123
- modules every entry re-exports, grouped into `wire/` for the format both
124
- sides speak, `contracts/` for the four store interfaces, `transport/` for the
125
- caller-to-server seam, and `util/` for helpers).
133
+ transport), `src/mock/` (the mock runtime), `src/testing/` (the test app),
134
+ `src/build/` (what a generator script runs at build time), and `src/shared/`
135
+ (isomorphic modules every entry re-exports, grouped into `wire/` for the
136
+ format both sides speak, `contracts/` for the five store and source
137
+ interfaces, `transport/` for the caller-to-server seam, and `util/` for
138
+ helpers).
126
139
  Directories are layers and imports only ever point down;
127
140
  [docs/api-core.md](./docs/api-core.md#layering) states the order and the test
128
141
  that enforces it.
@@ -137,8 +150,8 @@ guide that matches what you are building. The full index lives in
137
150
  | --- | --- |
138
151
  | [Getting started](./docs/getting-started.md) | The three-step path from a first API to a typed frontend call |
139
152
  | [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
140
- | [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, and non-HTTP invocations |
141
- | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal` |
153
+ | [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, crash reporting, and non-HTTP invocations |
154
+ | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal`, the signature file |
142
155
  | [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
143
156
  | [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
144
157
  | [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
@@ -147,11 +160,12 @@ guide that matches what you are building. The full index lives in
147
160
  | [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
148
161
  | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
149
162
  | [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
163
+ | [Direct uploads](./docs/uploads.md) | Files the browser posts straight to S3 with tickets the server signs, verified before they count |
150
164
  | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
151
165
  | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
152
166
  | [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
153
167
  | [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
154
- | [Exports reference](./docs/exports.md) | Every name the four entry points export, grouped by purpose |
168
+ | [Exports reference](./docs/exports.md) | Every name the five entry points export, grouped by purpose |
155
169
 
156
170
  ## Standalone modules
157
171
 
@@ -162,7 +176,7 @@ framework:
162
176
  | --- | --- | --- |
163
177
  | `html` / `xml` tags + `LambderTemplatingEngine` | [Templating](./docs/templating.md) | Type-safe tagged templates and a comment-only HTML template engine (build-pipeline-safe) |
164
178
  | `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension, auto language detection and on-demand language loading (isomorphic) |
165
- | `LambderDdbCache` | [DynamoDB cache](./docs/ddb-cache.md) | DynamoDB-backed compressed JSON cache with lease-based single-fill and grouped keys (server-only) |
179
+ | `LambderDdbCache` / `LambderMemoryCache` | [DynamoDB cache](./docs/ddb-cache.md) | JSON cache behind one `LambderCache` interface: DynamoDB-backed and compressed, with lease-based single-fill and grouped keys (server-only), or in memory for tests |
166
180
  | `LambderDdbRateLimiter` / `LambderMemoryRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | Fixed-window rate limiter, atomic per window, in DynamoDB (server-only) or in memory |
167
181
  | `LambderDdbIdempotencyStore` / `LambderMemoryIdempotencyStore` | [Idempotency store](./docs/ddb-idempotency.md) | Idempotency records with owner-checked claims, in DynamoDB (compressed replays, server-only) or in memory |
168
182
  | `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) |
@@ -170,14 +184,25 @@ framework:
170
184
  ## Versioning and changes
171
185
 
172
186
  Released versions and what each one changed are in
173
- [CHANGELOG.md](./CHANGELOG.md). The current major is v7, which moved the API
174
- pipeline into an isomorphic core, put the session layer behind a store
175
- interface, dropped the resolver argument from guards, and replaced the MSW
176
- adapter with a mock runtime. Every break and what to do about it is in the
177
- 7.0.0 entry. The compiler finds most of them. Three it cannot are named there:
178
- leftover `session` fields that `const` generics stop it from seeing, a
179
- `region` that went from required to optional, and mock handlers that now take
180
- the call context rather than the payload.
187
+ [CHANGELOG.md](./CHANGELOG.md). The current major is v8, which came out of a
188
+ review of 7.3.1: session writes that cannot undo a logout, API calls that must
189
+ be JSON, output schemas applied at runtime, rate limits that count IPv6 callers
190
+ by their /64 and custom keys after the guards, idempotency keys bound to the
191
+ request they were first sent with, and the same behavior on every gateway and
192
+ in the mock. Every break and what to do about it is in the 8.0.2 entry. The
193
+ compiler finds most of them. Fourteen it cannot are named there: hand-built
194
+ calls without a JSON Content-Type, hand-built answers without `apiVersion`,
195
+ handlers whose payload does not match their output schema, code that decoded
196
+ `ctx.path` itself, string routes that match case-sensitively, compression
197
+ behind a REST API, two more IAM actions (`UpdateItem` on the session table,
198
+ `GetItem` on the rate-limit table), custom-key limits charged after the
199
+ guards, idempotency keys bound to the request they were first sent with,
200
+ `errorMessage` always being an object, `credentials: true` needing named
201
+ origins, template slots and `html` interpolations refused or checked in
202
+ more attribute positions, `refreshSessionData()` throwing where it answered
203
+ null, and `z.ZodType<T>` annotations leaving a field unchecked. Every live
204
+ session is signed out once by the
205
+ upgrade. An app still on v6 goes through the 7.0.0 entry first.
181
206
 
182
207
  ## Contributing
183
208
 
@@ -1,40 +1,36 @@
1
1
  import type { LambderApiHttpAnswer } from "../shared/wire/LambderApiOutcome.js";
2
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.
3
+ * The core's output for one API call, before any transport-level
4
+ * finalization. The server adapter turns it into a Lambda response
5
+ * (compression, ETag, base64); the mock transport hands it to the caller as
6
+ * it is. The idempotency store persists and replays this shape, which is why
7
+ * it is plain data.
8
8
  *
9
- * Header names keep the casing they were written with; lookups are
10
- * case-insensitive (see getAnswerHeader), the way LambderResponse treats
11
- * them.
9
+ * Header names keep their written casing; lookups are case-insensitive (see
10
+ * getAnswerHeader), as in LambderResponse.
12
11
  */
13
12
  export type LambderApiAnswer = {
14
13
  statusCode: number;
15
14
  headers: Record<string, string[]>;
16
15
  body: string;
17
16
  /**
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.
17
+ * Finalization hints for the server adapter, so an answer that started as
18
+ * a LambderResponse loses nothing on the way: `isBodyBase64` marks a body
19
+ * that is base64 of binary bytes (never cached by the idempotency engine,
20
+ * never compressed); `compress` and `etag` are the LambderResponse flags.
21
+ * The mock runtime finalizes nothing and the stores drop the hints.
25
22
  */
26
23
  isBodyBase64?: boolean;
27
24
  compress?: boolean | "auto";
28
25
  etag?: boolean | "auto";
29
26
  };
30
27
  /**
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.
28
+ * An answer in the accessor form resolveApiOutcome() reads (the same view a
29
+ * fetch Response or a decoded Lambda result gets); the mock transport hands
30
+ * this to the caller.
34
31
  *
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.
32
+ * The body passes through as it stands, base64 hint or not: only the mock
33
+ * runtime's own answers reach here, and those are JSON envelopes. A binary
34
+ * body read through the JSON accessors would be of no use decoded anyway.
39
35
  */
40
36
  export declare const toHttpAnswer: (answer: LambderApiAnswer) => LambderApiHttpAnswer;
@@ -1,13 +1,12 @@
1
1
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
2
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.
3
+ * An answer in the accessor form resolveApiOutcome() reads (the same view a
4
+ * fetch Response or a decoded Lambda result gets); the mock transport hands
5
+ * this to the caller.
6
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.
7
+ * The body passes through as it stands, base64 hint or not: only the mock
8
+ * runtime's own answers reach here, and those are JSON envelopes. A binary
9
+ * body read through the JSON accessors would be of no use decoded anyway.
11
10
  */
12
11
  export const toHttpAnswer = (answer) => ({
13
12
  status: answer.statusCode,
@@ -3,9 +3,8 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
3
3
  /**
4
4
  * The context the API core needs from whoever runs it. The server's render
5
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.
6
+ * pipeline, policy engines and session controller touch nothing else, so
7
+ * they never learn which adapter they run under.
9
8
  *
10
9
  * - `session` is set by the pipeline on session APIs (and by the session
11
10
  * controller when a handler creates or ends one).
@@ -24,11 +23,25 @@ export type LambderApiCallContext<TSessionData = any> = {
24
23
  /** A fresh call context: no session, no guard data, nothing pending. */
25
24
  export declare const createApiCallContext: <TSessionData = any>() => LambderApiCallContext<TSessionData>;
26
25
  /**
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.
26
+ * Binds an adapter's tools onto one call context: `getters` run when read
27
+ * (`ctx.sessionController` is built over the object it was read from),
28
+ * `methods` are plain functions, and each is non-enumerable and bound to that
29
+ * object.
30
+ *
31
+ * Non-enumerable keeps a tool on the right object: a copy of the context (a
32
+ * server hook's `{ ...ctx, extra }`) carries none of them, rather than tools
33
+ * still bound to the original and its session. The adapter binds them again
34
+ * on a copy it continues with; configurable lets that replace them.
35
+ */
36
+ export declare const bindCallTools: (ctx: object, tools: {
37
+ getters?: Record<string, () => unknown>;
38
+ methods?: Record<string, (...args: never[]) => unknown>;
39
+ }) => void;
40
+ /**
41
+ * What one call recorded about itself while it ran, in order. Written as the
42
+ * call goes rather than assembled from each step's return, so a call refused
43
+ * partway still reports the guards that had already run (the mock's call log
44
+ * shows them for exactly the calls a developer is debugging).
32
45
  */
33
46
  export type LambderApiCallTrace = {
34
47
  /** The guards that ran, in order, including on a call that a later one refused. */
@@ -2,12 +2,30 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
2
2
  /** A fresh call context: no session, no guard data, nothing pending. */
3
3
  export const createApiCallContext = () => ({
4
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
5
+ // No prototype: guard names are the app's to choose, and on a plain
6
+ // object a guard named "toString" or "constructor" would read back as an
7
+ // inherited function to a handler checking whether that guard returned
9
8
  // anything.
10
9
  guardData: Object.create(null),
11
10
  responseHeaders: new LambderAnswerHeaders(),
12
11
  logList: [],
13
12
  });
13
+ /**
14
+ * Binds an adapter's tools onto one call context: `getters` run when read
15
+ * (`ctx.sessionController` is built over the object it was read from),
16
+ * `methods` are plain functions, and each is non-enumerable and bound to that
17
+ * object.
18
+ *
19
+ * Non-enumerable keeps a tool on the right object: a copy of the context (a
20
+ * server hook's `{ ...ctx, extra }`) carries none of them, rather than tools
21
+ * still bound to the original and its session. The adapter binds them again
22
+ * on a copy it continues with; configurable lets that replace them.
23
+ */
24
+ export const bindCallTools = (ctx, tools) => {
25
+ for (const [name, get] of Object.entries(tools.getters ?? {})) {
26
+ Object.defineProperty(ctx, name, { get, enumerable: false, configurable: true });
27
+ }
28
+ for (const [name, value] of Object.entries(tools.methods ?? {})) {
29
+ Object.defineProperty(ctx, name, { value, enumerable: false, configurable: true, writable: true });
30
+ }
31
+ };
@@ -6,9 +6,10 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
6
6
  * addApi/addSessionApi options carry, minus the handler, in a shape the mock
7
7
  * runtime can restate from a type-only contract. The schemas are optional
8
8
  * because the mock has none; when input is present, validation runs and the
9
- * handler sees the parsed payload. Output is read by nothing at request
10
- * time: it is part of the endpoint's signature (apiSignatureOf), which is
11
- * what a client's build is checked against.
9
+ * handler sees the parsed payload. Output is part of the endpoint's
10
+ * signature (apiSignatureOf), which is what a client's build is checked
11
+ * against; the server's resolver also parses every payload a handler
12
+ * answers through it before it is sent.
12
13
  */
13
14
  export type LambderApiDefinition = {
14
15
  name: string;
@@ -1,6 +1,7 @@
1
1
  import type { z } from "zod";
2
2
  import type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
3
3
  import { type LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
4
+ import type { LambderCrashDetail } from "../shared/wire/LambderCrashDetail.js";
4
5
  import type { LambderApiAnswer } from "./LambderApiAnswer.js";
5
6
  export declare const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
6
7
  /** The envelope's config plus the logList channel the call accumulated. */
@@ -40,14 +41,13 @@ export type LambderValidationAnswerBody = {
40
41
  };
41
42
  /**
42
43
  * 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.
44
+ * ZodError out. Not serialized as-is: zod 4 keeps `issues` non-enumerable,
45
+ * so JSON.stringify(zodError) would carry the issues only inside the message
46
+ * string, leaving a client's validation handler nothing to branch on.
47
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.
48
+ * zod's own `message` never ships: it is the whole issue tree re-serialized
49
+ * and would send every capped byte a second time. A generated summary takes
50
+ * its place on every answer.
51
51
  */
52
52
  export declare const validationAnswer: (zodError: z.ZodError, logList?: unknown[]) => LambderApiAnswer;
53
53
  /** No API is registered under the requested name: a refusal, not a 404, so a typed caller reads it. */
@@ -62,6 +62,11 @@ export declare const invalidPayloadAnswer: (apiVersion: string | null | undefine
62
62
  * The last-resort answer when the call crashed and nothing else could
63
63
  * answer: a 500 that is still an envelope, so a caller reads a structured
64
64
  * failure rather than a text page. The server sends it only when its global
65
- * error handler is absent or itself failed.
65
+ * error handler is absent or itself failed. `revealed` (the crash in full,
66
+ * with the call's logList) is passed only for a caller the app's
67
+ * `crashes.reveal` trusts.
66
68
  */
67
- export declare const crashAnswer: (apiVersion: string | null | undefined) => LambderApiAnswer;
69
+ export declare const crashAnswer: (apiVersion: string | null | undefined, revealed?: {
70
+ crash: LambderCrashDetail;
71
+ logList: unknown[];
72
+ }) => LambderApiAnswer;
@@ -1,14 +1,13 @@
1
- import { LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
1
+ import { LAMBDER_REFUSAL_CODES, refusalMessageOf } from "../shared/wire/LambderApiRefusal.js";
2
2
  import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
3
3
  /*
4
4
  * The one place the API envelope is written, and the one mapping from each
5
5
  * kind of protocol outcome onto an answer: a success, a thrown refusal, a
6
6
  * rejected input, an unknown name, a missing session, a stale client, a
7
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.
8
+ * res.api(), the mock runtime and the pipeline all build through these
9
+ * functions, so server and mock cannot drift on a single byte of the wire
10
+ * format. Pure: no Node built-ins, no response classes.
12
11
  */
13
12
  export const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
14
13
  /**
@@ -22,12 +21,13 @@ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionE
22
21
  ...(versionExpired ? { versionExpired } : {}),
23
22
  ...(sessionExpired ? { sessionExpired } : {}),
24
23
  ...(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.
24
+ // Presence, not truthiness: these channels carry app values, and an app
25
+ // that refuses with errorMessage: "" (or 0) meant to say something. The
26
+ // flags above are booleans, where false and absent mean the same. An
27
+ // errorMessage goes out as a message object whatever form it was written
28
+ // in, so every reader meets one shape.
29
29
  ...(message !== undefined ? { message } : {}),
30
- ...(errorMessage !== undefined ? { errorMessage } : {}),
30
+ ...(errorMessage !== undefined ? { errorMessage: refusalMessageOf(errorMessage) } : {}),
31
31
  ...(crash !== undefined ? { crash } : {}),
32
32
  ...(logList?.length ? { logList } : {}),
33
33
  });
@@ -63,11 +63,11 @@ export const refusalAnswer = (err, apiVersion, logList) => envelopeAnswer(buildA
63
63
  */
64
64
  const MAX_VALIDATION_ISSUES = 50;
65
65
  /**
66
- * What the whole issue list may cost, serialized. The count cap alone bounds
66
+ * What the whole issue list may cost, serialized. A count cap alone bounds
67
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
68
+ * posted, so a strictObject could answer a 1MB body with a 4MB one,
69
+ * unauthenticated and before any guard ran, and past maxResponseBytes the 422
70
+ * would become a 500. The amplification is measured in bytes, so bytes are
71
71
  * capped.
72
72
  */
73
73
  const MAX_VALIDATION_ISSUES_BYTES = 32_000;
@@ -78,10 +78,9 @@ const MAX_VALIDATION_TEXT_CHARS = 200;
78
78
  const utf8Encoder = new TextEncoder();
79
79
  const clampText = (value) => value.length > MAX_VALIDATION_TEXT_CHARS ? `${value.slice(0, MAX_VALIDATION_TEXT_CHARS)}...` : value;
80
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.
81
+ * One issue with its own strings and lists bounded. Applied to every field
82
+ * rather than to known ones like `keys`, because any issue a schema authors
83
+ * itself may carry a list or a message whose size the client chose.
85
84
  */
86
85
  const clampIssueValue = (value) => {
87
86
  if (typeof value === "string")
@@ -91,10 +90,9 @@ const clampIssueValue = (value) => {
91
90
  return value;
92
91
  };
93
92
  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.
93
+ // The mapping carries every field through in kind, the union's
94
+ // discriminant included; the cast states that, since a mapped object
95
+ // cannot express it in the type system.
98
96
  Object.fromEntries(Object.entries(issue).map(([key, value]) => [key, clampIssueValue(value)]));
99
97
  /** The issue list the body may carry: clamped, then cut to the byte budget, whole issues from the end. */
100
98
  const boundIssueList = (all) => {
@@ -108,9 +106,9 @@ const boundIssueList = (all) => {
108
106
  const size = utf8Encoder.encode(JSON.stringify(clamped)).length;
109
107
  if (bytes + size > MAX_VALIDATION_ISSUES_BYTES) {
110
108
  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.
109
+ // A first issue over the budget on its own still ships the three
110
+ // fields every issue carries, so a client always has a code and a
111
+ // path to branch on.
114
112
  if (issues.length === 0) {
115
113
  issues.push({ code: clamped.code, path: clamped.path.slice(0, MAX_VALIDATION_LIST_ENTRIES), message: clampText(clamped.message) });
116
114
  }
@@ -132,14 +130,13 @@ const summarizeIssues = (total, listed, trimmed) => {
132
130
  };
133
131
  /**
134
132
  * 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.
133
+ * ZodError out. Not serialized as-is: zod 4 keeps `issues` non-enumerable,
134
+ * so JSON.stringify(zodError) would carry the issues only inside the message
135
+ * string, leaving a client's validation handler nothing to branch on.
139
136
  *
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.
137
+ * zod's own `message` never ships: it is the whole issue tree re-serialized
138
+ * and would send every capped byte a second time. A generated summary takes
139
+ * its place on every answer.
143
140
  */
144
141
  export const validationAnswer = (zodError, logList) => {
145
142
  const { issues, trimmed } = boundIssueList(zodError.issues);
@@ -175,6 +172,8 @@ export const invalidPayloadAnswer = (apiVersion, message) => envelopeAnswer(buil
175
172
  * The last-resort answer when the call crashed and nothing else could
176
173
  * answer: a 500 that is still an envelope, so a caller reads a structured
177
174
  * failure rather than a text page. The server sends it only when its global
178
- * error handler is absent or itself failed.
175
+ * error handler is absent or itself failed. `revealed` (the crash in full,
176
+ * with the call's logList) is passed only for a caller the app's
177
+ * `crashes.reveal` trusts.
179
178
  */
180
- export const crashAnswer = (apiVersion) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { errorMessage: "Internal server error." }), { statusCode: 500 });
179
+ export const crashAnswer = (apiVersion, revealed) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { errorMessage: "Internal server error.", ...revealed }), { statusCode: 500 });