lambder 14.1.1 → 15.0.2

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 (158) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +14 -17
  3. package/dist/api/LambderApiDeclarations.d.ts +11 -5
  4. package/dist/api/LambderApiDefinition.d.ts +2 -1
  5. package/dist/api/LambderApiEnvelope.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.js +8 -2
  7. package/dist/api/LambderApiGuards.d.ts +44 -5
  8. package/dist/api/LambderApiGuards.js +27 -0
  9. package/dist/api/LambderApiIdempotency.d.ts +14 -7
  10. package/dist/api/LambderApiIdempotency.js +39 -27
  11. package/dist/api/LambderApiPipeline.d.ts +25 -11
  12. package/dist/api/LambderApiPipeline.js +34 -17
  13. package/dist/api/LambderApiRateLimits.d.ts +28 -21
  14. package/dist/api/LambderApiRateLimits.js +55 -35
  15. package/dist/api/LambderApiRefusals.d.ts +10 -4
  16. package/dist/api/LambderApiRefusals.js +31 -5
  17. package/dist/api/LambderApiRequest.d.ts +21 -1
  18. package/dist/api/LambderApiRequest.js +31 -2
  19. package/dist/build/checkApiRefusals.d.ts +8 -3
  20. package/dist/build/checkApiRefusals.js +80 -29
  21. package/dist/build/contractPrinterProcess.d.ts +11 -0
  22. package/dist/build/contractPrinterProcess.js +17 -0
  23. package/dist/build/generateApiFiles.d.ts +26 -5
  24. package/dist/build/generateApiFiles.js +68 -7
  25. package/dist/build/writeApiSchemas.d.ts +109 -0
  26. package/dist/build/writeApiSchemas.js +308 -0
  27. package/dist/build.d.ts +3 -1
  28. package/dist/build.js +2 -0
  29. package/dist/client/LambderCaller.d.ts +25 -11
  30. package/dist/client/LambderCaller.js +57 -49
  31. package/dist/client/LambderUploadRunner.d.ts +55 -27
  32. package/dist/client/LambderUploadRunner.js +89 -34
  33. package/dist/client.d.ts +6 -6
  34. package/dist/client.js +2 -2
  35. package/dist/core/Lambder.d.ts +31 -21
  36. package/dist/core/Lambder.js +79 -23
  37. package/dist/core/LambderCallSummary.d.ts +1 -1
  38. package/dist/core/LambderContext.d.ts +59 -7
  39. package/dist/core/LambderContext.js +31 -15
  40. package/dist/core/LambderCrashHandling.d.ts +5 -0
  41. package/dist/core/LambderCrashHandling.js +11 -1
  42. package/dist/core/LambderCreateOptions.d.ts +28 -17
  43. package/dist/core/LambderCreateOptions.js +14 -7
  44. package/dist/core/LambderResolver.d.ts +3 -2
  45. package/dist/core/LambderResponseBuilder.d.ts +20 -9
  46. package/dist/core/LambderResponseBuilder.js +39 -8
  47. package/dist/core/LambderTemplatingEngine.d.ts +24 -6
  48. package/dist/core/LambderTemplatingEngine.js +23 -3
  49. package/dist/index.d.ts +15 -12
  50. package/dist/index.js +7 -5
  51. package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
  52. package/dist/invoke/LambderInvokeCaller.js +47 -9
  53. package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
  54. package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
  55. package/dist/mock/LambderMockAdapterDoor.js +10 -0
  56. package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
  57. package/dist/mock/LambderMockApiSchemas.js +150 -0
  58. package/dist/mock/LambderMockApp.d.ts +56 -48
  59. package/dist/mock/LambderMockApp.js +107 -62
  60. package/dist/mock/LambderMockBrowserCookies.js +18 -5
  61. package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
  62. package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
  63. package/dist/mock/LambderMockFailureInjector.js +12 -8
  64. package/dist/mock/LambderMockTypes.d.ts +8 -4
  65. package/dist/mock/lambderMockInvokeTransport.js +2 -9
  66. package/dist/mock/lambderMockMswHandler.d.ts +19 -13
  67. package/dist/mock/lambderMockMswHandler.js +25 -22
  68. package/dist/mock.d.ts +1 -0
  69. package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
  70. package/dist/secrets/LambderOneShotSecrets.js +6 -36
  71. package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
  72. package/dist/secrets/LambderPasswordHasher.js +168 -0
  73. package/dist/session/LambderSessionController.d.ts +37 -4
  74. package/dist/session/LambderSessionController.js +36 -6
  75. package/dist/session/LambderSessionManager.d.ts +33 -3
  76. package/dist/session/LambderSessionManager.js +55 -16
  77. package/dist/shared/LambderHtml.d.ts +23 -4
  78. package/dist/shared/LambderHtml.js +17 -5
  79. package/dist/shared/LambderI18n.d.ts +106 -16
  80. package/dist/shared/LambderI18n.js +259 -74
  81. package/dist/shared/contracts/LambderCache.d.ts +14 -1
  82. package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
  83. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  84. package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
  85. package/dist/shared/util/LambderBackoffTimer.js +40 -12
  86. package/dist/shared/util/LambderCallAbort.d.ts +22 -6
  87. package/dist/shared/util/LambderCallAbort.js +37 -9
  88. package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
  89. package/dist/shared/util/LambderErrorReportMark.js +22 -0
  90. package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
  91. package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
  92. package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
  93. package/dist/shared/util/LambderSignedClaims.js +54 -0
  94. package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
  95. package/dist/shared/util/LambderSwappableInstances.js +54 -0
  96. package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
  97. package/dist/shared/util/LambderTestingDoors.js +6 -2
  98. package/dist/shared/util/LambderTextDigest.d.ts +5 -5
  99. package/dist/shared/util/LambderTextDigest.js +5 -5
  100. package/dist/shared/wire/LambderApiContract.d.ts +1 -1
  101. package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
  102. package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
  103. package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
  104. package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
  105. package/dist/shared/wire/LambderApiOutcome.js +1 -1
  106. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
  107. package/dist/shared/wire/LambderApiRefusal.js +3 -3
  108. package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
  109. package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
  110. package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
  111. package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
  112. package/dist/shared/wire/LambderCallOutcome.js +3 -2
  113. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
  114. package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
  115. package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
  116. package/dist/shared/wire/LambderInvokeApiId.js +3 -3
  117. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
  118. package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
  119. package/dist/stores/LambderCacheValues.d.ts +2 -5
  120. package/dist/stores/LambderDdbCache.d.ts +15 -9
  121. package/dist/stores/LambderDdbCache.js +25 -6
  122. package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
  123. package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
  124. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
  125. package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
  126. package/dist/stores/LambderDdbSdk.d.ts +59 -29
  127. package/dist/stores/LambderDdbSdk.js +116 -44
  128. package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
  129. package/dist/stores/LambderDdbSessionStore.js +133 -68
  130. package/dist/stores/LambderMemoryCache.d.ts +2 -2
  131. package/dist/stores/LambderS3FileSource.d.ts +3 -2
  132. package/dist/stores/LambderS3FileSource.js +11 -13
  133. package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
  134. package/dist/stores/LambderS3UploadBucket.js +23 -0
  135. package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
  136. package/dist/stores/LambderStoredText.d.ts +26 -0
  137. package/dist/stores/LambderStoredText.js +25 -0
  138. package/dist/testing/LambderConformanceRunner.d.ts +9 -2
  139. package/dist/testing/LambderConformanceRunner.js +9 -2
  140. package/dist/testing/LambderTestApp.d.ts +65 -7
  141. package/dist/testing/LambderTestApp.js +101 -6
  142. package/dist/testing/LambderTestVisitor.d.ts +43 -12
  143. package/dist/testing/LambderTestVisitor.js +35 -11
  144. package/dist/testing/bootLambdaPackage.d.ts +105 -0
  145. package/dist/testing/bootLambdaPackage.js +265 -0
  146. package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
  147. package/dist/testing/lambdaPackageBootProcess.js +130 -0
  148. package/dist/testing/lambderCacheConformance.d.ts +26 -0
  149. package/dist/testing/lambderCacheConformance.js +287 -0
  150. package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
  151. package/dist/testing/lambderCacheStorageConformance.js +159 -0
  152. package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
  153. package/dist/testing/lambderSessionStoreConformance.js +3 -3
  154. package/dist/testing.d.ts +7 -1
  155. package/dist/testing.js +5 -0
  156. package/package.json +1 -6
  157. package/dist/shared/util/boundKeyField.d.ts +0 -20
  158. package/dist/shared/util/boundKeyField.js +0 -34
package/CHANGELOG.md CHANGED
@@ -9,6 +9,435 @@ sit on its first published patch, and later patches list only what they changed.
9
9
  Releases up to 3.2.6 carry git tags; the ones after it were published without
10
10
  one, so versions are not cross-linked to tag comparisons here.
11
11
 
12
+ ## [15.0.2] - 2026-10-02
13
+
14
+ A patch: two fixes for apps with a large contract.
15
+
16
+ ### Fixed
17
+
18
+ - **Reading a failure compiles for a contract of hundreds of refusal codes.**
19
+ `failure.reason`, `failure.refusal`, `outcome.ok` and every other read of a
20
+ `LambderApiFailure` or `LambderApiOutcome` over a contract's refusal
21
+ messages failed with TS2590 ("union type too complex to represent") once
22
+ the vocabulary held a few hundred codes, and so did a `LambderCaller` whose
23
+ `fetchEndedHandler` or `errorHandler` read its argument: the refusal arm
24
+ declared its message both on the fields every failure shares and on the arm
25
+ itself, and a read of the intersection crossed the message union with
26
+ itself. Each arm now declares it once. The types describe the same values.
27
+ - **`checkApiRefusals` no longer reports a mock guard's unused codes.** A mock
28
+ guard declares exactly its server guard's codes, which `create()` holds it
29
+ to against `guardDeclarations`, so one that raises fewer (a captcha stand-in
30
+ letting every token through) got an `unused` finding it could not act on.
31
+ Its codes are still checked the other way: one it raises and may not send
32
+ is `undeclared`.
33
+
34
+ ## [15.0.1] - 2026-10-02
35
+
36
+ A major. Fixes for crashes, misconfigurations that passed silently, and places
37
+ where the mock and the test kit drifted from the server; and capabilities apps
38
+ were writing for themselves: an `aborted` outcome for a call the page cancels,
39
+ upload retries that keep their ticket, request provenance under the origin
40
+ proof, digested key fields at rest, guard inputs in the idempotency identity,
41
+ plural forms, typed template data, dev-only schemas the mock validates against,
42
+ a test app that keeps an app's own caches, one-shot secret stores, upload
43
+ buckets and invoke callers off AWS by default, a boot check for
44
+ a built deployment package, and argon2id password hashing. Every break is
45
+ named under Changed.
46
+
47
+ ### Added
48
+
49
+ - **`randomCode(alphabet, length)`**, from `lambder` and `lambder/client`
50
+ beside `randomSecret`: a code a person types or reads out, drawn evenly from
51
+ the alphabet with the cryptographic random source. Bytes past the largest
52
+ multiple of the alphabet's size are discarded rather than folded in with a
53
+ modulo, which favors the first characters. An alphabet that is not 2 to 256
54
+ distinct characters, or a length that is not a positive integer, throws.
55
+ `LambderOneShotSecrets` draws its codes with it.
56
+ - **`LambderPasswordHasher`**, from `lambder`: passwords at rest as argon2id
57
+ through node's own `crypto.argon2` (Node 24.7 and later), in the PHC string
58
+ every argon2 library reads and writes, so no native addon ships in the
59
+ deployment package. `hash(password)` writes argon2id under the instance's
60
+ cost (`memoryKib`, `passes`, `parallelism`; default 64 MiB, 3, 4) with a
61
+ fresh salt. `verify(stored, password)` reads the variant and cost from the
62
+ stored string, so it verifies argon2id, argon2i and argon2d strings other
63
+ libraries wrote, parameters in any order, and answers false rather than
64
+ throwing for anything that is not one (`null` for an account with no
65
+ password, `undefined` for one that does not exist), still computing one hash
66
+ under the instance's cost so the time taken does not say which accounts
67
+ exist. A cost has ceilings, for what an instance writes and what it
68
+ verifies alike: 2 GiB of memory, 4 GiB of memory over all passes, 255 lanes;
69
+ a stored string over them matches no password rather than stalling every
70
+ sign-in on its account. `needsRehash(stored)` says when a stored hash was
71
+ written under another variant or cost, so a raised cost reaches old
72
+ passwords at their next sign-in. A runtime without argon2 throws at
73
+ construction rather than failing every sign-in. New type:
74
+ `LambderPasswordHasherOptions`.
75
+ - **`fetchSession({ refreshData: true })` and
76
+ `fetchSessionIfExists({ refreshData: true })`** on the session controller,
77
+ and `renewSession(session, { refreshData: true })` on the manager: renew the
78
+ session's data on that read whatever its deadline, running the `dataRefresh`
79
+ callback and the store write once. A read followed by `refreshSessionData()`
80
+ ran both twice whenever the data was due. Without `dataRefresh` they throw.
81
+ - **`LambderRenderContextOf<typeof app>` and
82
+ `LambderSessionRenderContextOf<typeof app>`** name one instance's render
83
+ context, with its session data and rate-limit policy names, for typing a
84
+ helper in another file without writing out `LambderRenderContext`'s
85
+ parameters. Contexts from API handlers, routes, hooks and guards are all
86
+ assignable to them.
87
+ - **`apisWithGuard(apiOptions, guard)`**, from `lambder` and `lambder/client`:
88
+ the names of the APIs whose guards option names a guard, typed as
89
+ `LambderApisWithGuard` and in the table's order, so a test can loop over the
90
+ APIs behind a guard without casting the generated table.
91
+ - **`lambderCacheConformance` and `lambderCacheStorageConformance`**
92
+ (`lambder/testing`), with their options types: the rules every `LambderCache`
93
+ keeps, and the rules for the `LambderCacheStorage` under
94
+ `LambderStorageBackedCache`. An app that writes its own cache or storage
95
+ checks it against the rules Lambder's own meet. The storage suite targets
96
+ the usual database slips: jsonb reordering keys, case-insensitive or
97
+ space-padding collations, LIKE wildcards in a listing prefix, partitions
98
+ matched by prefix.
99
+ - **`LambderI18nReadonlyInstance<TContract>`**: the reading members of any
100
+ LambderI18n instance of one contract, whatever its language set, without the
101
+ members that switch languages, load or register dictionaries, touch the
102
+ document or build extensions. Code that only reads translations takes any
103
+ such instance without restating its members.
104
+ - **`jsonScript({ type: "application/ld+json" }, data)`** writes JSON-LD with
105
+ the same escaping (`<`, U+2028, U+2029) as the hydration form.
106
+ `jsonScript(id, data)` is unchanged, the options form `{ id, type? }` writes
107
+ `application/json`, and any other type is refused. New type:
108
+ `LambderJsonScriptOptions`.
109
+ - **`LambderBackoffTimer.retries`**: the retries `retry` and `wait` have
110
+ scheduled since the last `reset()`, so a loop that gives up after so many
111
+ reads the timer instead of keeping a counter. `LambderUploadRunner` counts
112
+ its storage retries this way.
113
+ - **`LambderMockApp.pageCookieJar`**: the page's cookie jar, owned by the
114
+ runtime and emptied by `reset()` (see Changed).
115
+ - **`readApiEnvelopeText(text, info, apiName)`**: `readApiEnvelope` for an
116
+ adapter that holds only the body's text. An empty text is an empty
117
+ envelope; text that is not JSON is flagged as no envelope (see Fixed).
118
+ - **`ctx.arrivedVia`** (`LambderRequestArrival`): how the request reached the
119
+ function. `"proxy"` with a valid origin proof, `"direct"` when the proof is
120
+ configured and missing or wrong, `"invoke"` for a Lambda invoke (read from
121
+ the event's `requestContext.apiId`, which no HTTP client can set), and
122
+ `"unverified"` when no proof is configured. An invoke-only endpoint checks
123
+ it rather than the `x-lambder-invoke` header, which any client can send.
124
+ - **`originProof.proxyHeaders`**: headers only the proxy may write. On a
125
+ request without a valid proof they are removed from `ctx.headers`,
126
+ `ctx.header()` and the API request's headers, with the trusted client-IP and
127
+ host headers, so a handler reads the proxy's value or nothing. An origin
128
+ proof with no trusted headers is accepted, since `arrivedVia` is useful on
129
+ its own.
130
+ - **`lambder/rate-limited` carries typed data**, `{ policy, retryAfterSeconds }`
131
+ (`LambderRateLimitRefusalData`), from the server and the mock alike, as its
132
+ own arm of `LambderRefusalMessage`, so a client narrows `message.data` and
133
+ words each limit its own way. The policy's name reaches every client, so
134
+ name policies fit to show. The mock's injected rate limit takes `policy`.
135
+ - **Plural forms in LambderI18n.** An entry may be a plural entry keyed by
136
+ `Intl.PluralRules` category with `other` required,
137
+ `{ one: "{count} item", other: "{count} items" }`, and `t(key, { count })`
138
+ picks the form under the rules of the text's language, so a language with
139
+ six categories (Arabic) translates as well as English. A plural key needs a
140
+ numeric `count` at compile time beside its tokens. The default language's
141
+ entry decides a key's kind: a text key takes texts, a plural key takes
142
+ plural entries or a plain text standing for `other`. Malformed entries are
143
+ refused where a dictionary arrives. Category coverage depends on each
144
+ runtime's CLDR data, so it never fails at runtime: a count whose category an
145
+ entry lacks shows its `other` form. **`checkPluralCoverage()`** is the strict
146
+ check for tests and builds: it loads every language and rejects with every
147
+ gap by key, language and missing categories. New types:
148
+ `LambderI18nDictionaryEntry`, `LambderI18nPluralEntry`,
149
+ `LambderI18nPluralCategory`.
150
+ - **Typed template data.** `LambderTemplatingEngine<TNames>`,
151
+ `fromFile<TNames>` and `res.templateFile<TNames>(...)` (and on `res.die`)
152
+ type the data by the template's slot and condition names, so a misspelled
153
+ key is a compile error; `LambderTemplateData` takes the names. New type
154
+ `LambderTemplateFileOptions`. The runtime check is under Changed.
155
+ - **`writeApiSchemas`** (`lambder/build`, and `schemas` in `generateApiFiles`):
156
+ every API's input schema as a client posts it and output schema as a client
157
+ receives it, as JSON Schema in a module of plain data, for the mock in
158
+ development. It lists every refinement, transform, pipe, computed default and
159
+ string check the file cannot carry (a regex's flags, a URL's protocol or
160
+ hostname rule), never writes a check the mock would read back stricter than
161
+ the server (a guid as a uuid, an email under its own pattern), and throws,
162
+ naming the API and field, for anything JSON Schema cannot represent. The schemas come from the new instance method
163
+ `apiSchemaEntries()`. New types: `LambderApiSchemasSource`,
164
+ `LambderApiSchemasFileOptions`, `LambderApiSchemasFileResult`,
165
+ `LambderApiSchemaLoss`.
166
+ - **The mock's `apiSchemas` option** takes that module. The mock then validates
167
+ every call's input against the server's schema and refuses it as the server
168
+ does (422, or what `onInvalidInput` answers), hands the handler the payload
169
+ with undeclared keys dropped and defaults filled, and parses every answer
170
+ through the output schema, so fields the server strips no longer reach the
171
+ client in development. Each schema is rebuilt with `z.fromJSONSchema` the
172
+ first time it is needed; an entry's own `input` schema applies after it. The
173
+ module names every endpoint, so import it only from the mock's setup. New
174
+ types (`lambder/mock`): `LambderApiSchemaEntries`, `LambderApiSchemaEntry`,
175
+ `LambderJsonSchema`.
176
+ - **`bootLambdaPackage(options)`** (`lambder/testing`): boots a built
177
+ deployment package in a fresh node process as Lambda does, hands the handler
178
+ API calls (`{ api, payload }`, turned into gateway or invoke events) or raw
179
+ events, and reports each answer (an outcome for an API call), the import
180
+ time and memory, and the phase and call that failed. Imports the package
181
+ makes are held to the package, so a dependency it does not carry fails here
182
+ rather than after a deploy; what the runtime supplies (the AWS SDK by
183
+ default) resolves from an install outside it. The process sees only `PATH`
184
+ and the given `env`. Needs Node 22.15 or later. New types:
185
+ `LambderPackageBootOptions`, `LambderPackageBootCall`,
186
+ `LambderPackageBootResult`, `LambderPackageBootCallResult`,
187
+ `LambderPackageBootMeasurements`.
188
+ - **`testApp.assertNoCrashesAfterEach(afterEach)`** fails any test the app
189
+ crashed in, listing each crash with its stack, in place of a crash check
190
+ repeated in every test file. **`testApp.memoryTwinOf(store)`** hands back the
191
+ in-memory copy a swapped store answers from, to seed or inspect it. New
192
+ types `LambderTestVisitorEndpoint` and `LambderTestVisitorGroupCalls`.
193
+ - **`LambderApiFailure`**, from `lambder/client` and `lambder`: the failure
194
+ side of an API outcome, which `errorHandler` now receives (see Changed).
195
+
196
+ ### Changed
197
+
198
+ - **Breaking: a `minApiVersion` above `apiVersion` throws at creation**, on
199
+ the server and in the mock, like every other misconfiguration. It was
200
+ clamped to `apiVersion` with a console warning.
201
+ - **Breaking (types): `lambder.apiPath`, `lambder.apiVersion` and
202
+ `lambder.files` are `readonly`.** The pipeline, crash handling and public
203
+ files keep what they were built with, so a later write left the instance
204
+ disagreeing with itself.
205
+ - **Breaking (types): `LambderCache.getOrSet` takes
206
+ `LambderCacheGetOrSetOptions`**, which adds `leaseSeconds` and
207
+ `waitForFillMs` to `ttlSeconds`, so code typed against the interface can tune
208
+ the DynamoDB cache's fill lease. The type was `LambderDdbCacheGetOrSetOptions`;
209
+ that name is gone. Every cache already checked these options at runtime.
210
+ - **Breaking (types): `LambderSessionManager.expireSessionDataAllByKey()`
211
+ returns `Promise<void>`**, as the controller's does, in place of a boolean
212
+ that was always `true`.
213
+ - **Breaking (API core): `readApiEnvelope` takes the parsed body as
214
+ `unknown`** and a `bodyNotJson` flag, and flags a body that is not a plain
215
+ object on the request's new `invalidEnvelope` field. A `null` body is
216
+ flagged where it read as an empty envelope; an absent body still reads as
217
+ an empty one.
218
+ - **The session controller's `endSessionAll()` and `deleteSessionAllByKey()`
219
+ return `Promise<boolean>`**, passing on the manager's answer: false means a
220
+ session rotating in a tight loop raced every pass of the delete and may
221
+ still stand, so run `deleteSessionAllByKey()` again. The password-change
222
+ recipe in the sessions docs checks it.
223
+ - **Breaking (mock): with no jar, `signIn` and `signOut` use
224
+ `pageCookieJar`, and `lambderMockMswHandler` uses it unless given
225
+ `cookieJar`.** `signIn(key, data)` alone now signs the page in behind the
226
+ MSW worker; the session used to go to no jar and every call answered
227
+ `sessionExpired`.
228
+ - **Breaking (mock): `mock.guard` from a mock that declared a refusal
229
+ vocabulary is bound to it**, as the server init's `guard` is: `ctx.refuse` is
230
+ typed to the guard's own `refusals`, and a code outside the vocabulary fails
231
+ to compile and throws when the guard is built. `checkApiRefusals` checks
232
+ these guards rather than listing them as unchecked.
233
+ - **Breaking (testing): `lambderTestApp` throws when `session.store`,
234
+ `rateLimits.limiter` or `idempotency.store` is given for an app created
235
+ without that subsystem**, as it already did for `files`. The store was
236
+ silently dropped and the suite ran over nothing.
237
+ - **Breaking: `LambderBackoffTimer` refuses at construction** a `baseMs` of 0
238
+ or less (every wait was 0 ms), a `baseMs` or `maxMs` above 2147483647 ms
239
+ (which `setTimeout` fires at once), and an unknown `jitter`.
240
+ `LambderUploadRunner` therefore refuses a `storageRetry.baseDelayMs` of 0
241
+ when it is built.
242
+ - **LambderI18n compares language codes case-insensitively**, as BCP 47 tags
243
+ are, and always answers with the code as registered: a browser's `pt-br`, a
244
+ detector's `PT-BR` and `setLanguage("pt-br")` select a registered `pt-BR`,
245
+ as do `forLanguage`, `loadLanguage` and `registerDictionary`.
246
+ `isLanguageCode` stays exact, since it narrows its argument. Breaking only
247
+ for a config that registers two codes differing only in case, which
248
+ `createLambderI18n` refuses.
249
+ - **`LambderCaller`'s `apiPath` is optional** and defaults to `"/api"`, the
250
+ server's own default, from the constant `LambderInvokeCaller` uses.
251
+ - **`LambderDdbIdempotencyStore.complete()` answers `"too-large"` for a body
252
+ over 32 MB**, however small it compresses. Such a body was stored and then
253
+ refused on every replay.
254
+ - **One-shot secret alphabets are counted in code points**, so an alphabet
255
+ of characters outside the basic plane is accepted, where it was refused as
256
+ not distinct.
257
+ - **`addHook("created", fn, priority)` honors the priority** like the other
258
+ hooks: lower runs first, and hooks of one priority run in the order they
259
+ were added.
260
+
261
+ - **Breaking: `res.html`, `res.status`, `res.status404` and `res.xml` (and
262
+ `res.die.*`) take safe markup only**, what `html`/`xml`, `raw()` and
263
+ `jsonScript()` build. A plain string is a compile error and throws, naming
264
+ the tag, `raw()` and `res.text`, so a route cannot reflect request text into
265
+ a page unescaped. Plain text goes out with `res.text(body, { statusCode })`.
266
+ The framework's own 401 for a session route without a session and
267
+ `res.file`'s 404 are `text/plain`.
268
+ - **Breaking: rendering a template with a data key it has no slot or
269
+ condition for throws**, naming the key and the template's names;
270
+ `res.templateFile` also names the file. A slot renamed in the HTML dropped
271
+ the server's content silently.
272
+ - **Breaking: `finalizeResponse`, `answerFromResponse`, `responseFromAnswer`
273
+ and `LambderFinalizeOptions` are no longer exported.** They take internal
274
+ options no consumer could build.
275
+ - **Breaking: a call aborted through its own `signal` fails with the new reason
276
+ `aborted`**, on `LambderCaller` and `LambderInvokeCaller`, in place of
277
+ `network`, and is never handed to `errorHandler` or `onFailure`, so a page
278
+ can cancel a superseded read without a "could not reach the server" alert.
279
+ `fetchEndedHandler` still hears it, a `timeout` is still reported, and
280
+ whichever aborted the call first names it. A signal that aborts with a
281
+ `TimeoutError` (`AbortSignal.timeout()`, alone or inside `AbortSignal.any()`)
282
+ carries a deadline rather than the page's choice, and fails the call as
283
+ `timeout`. A key scope treats an aborted call as not sent before sending and
284
+ possibly used after. The upload runner's `cancelled` reason is `aborted` too.
285
+ - **`errorHandler` is called as `(error, failure)`**, `failure` being the
286
+ call's typed failure (`LambderApiFailure`), the reload-loop and
287
+ missing-handler cases included; a one-argument handler keeps working.
288
+ **Breaking:** `fetchEndedHandler`'s `fetchResult` is the call's outcome, the
289
+ object `apiOutcome()` resolves to, in place of an envelope, an Error or null.
290
+ - **Breaking: `LambderUploadRunner`'s `requestTicket` and `confirmUpload`
291
+ return the caller's outcome** (`caller.x.y.outcome()`), and the runner
292
+ retries their `network`, `timeout` and `server` failures on its backoff timer
293
+ with the same ticket, so a dropped confirm after the bytes are stored no
294
+ longer ends the upload or leaves an orphan object. Each step gets every
295
+ attempt. The ticket endpoint's output, which carries `ticket`, is handed to
296
+ `confirmUpload` in place of `{ ticket, reference }`, and `LambderUploadError`
297
+ carries the failed call's outcome as `callFailure`. A retried call may
298
+ follow one that ran and lost its answer, so a confirm with an effect beyond
299
+ marking its record is declared idempotent and called with one key per
300
+ upload (`confirm-${invoiceId}`); the uploads docs show it.
301
+ - **A `LambderInvokeError` whose failure `onFailure` already received is not
302
+ reported again** by the calling instance's `crashes.report`; the request is
303
+ still answered as a crash. Apps drop their `isLambderInvokeError` skip.
304
+ - **Breaking: `rateLimitRefusal(detail, { policy, retryAfterSeconds }, words?)`**
305
+ replaces `(detail, retryAfterSeconds, message?)`, and
306
+ `DEFAULT_RATE_LIMIT_REFUSAL` is the default words alone. A refusal carrying
307
+ `lambder/rate-limited` without its data is a crash, and `res.apiRefusal`
308
+ refuses the code. An exhaustive switch over `LambderRefusalMessage` asserts
309
+ `never` on the message rather than on `message.code`.
310
+ - **Breaking: caller-controlled key fields are digests at rest.** Every
311
+ caller-controlled field of a rate-limit tracker key and an idempotency scope
312
+ (the address, session key, custom key, `callerIdentity`, posted idempotency
313
+ key) is written as a fixed-length digest at any length; it was readable up
314
+ to 1,024 bytes, so emails and tokens sat in partition keys. With sessions
315
+ configured it is an HMAC under an HKDF subkey of the `sessionSalt`; without
316
+ them a plain SHA-256, which a table reader can test guesses against. API and
317
+ policy names stay readable. Counters and idempotency records start afresh
318
+ once on upgrade, and again whenever `sessionSalt` changes.
319
+ - **Breaking: a `guardInput` guard's value counts in an idempotency key's
320
+ request**, beside the payload: the same key with another store or tenant is
321
+ 409 `lambder/idempotency-key-reused`, where it replayed the first answer. A
322
+ guard whose input can differ between attempts of one operation declares
323
+ `singleUseInput: true`, so a retry still replays: a single-use proof (a fresh
324
+ captcha token, a one-time code) or a credential refreshed between attempts
325
+ (a short-lived token). Apps with such guards add it; without it the retry is
326
+ refused and the person's next attempt runs the operation again.
327
+ - **Breaking: `LambderDdbSessionStore` takes a `DynamoDBClient`**, like every
328
+ other DynamoDB store, and `@aws-sdk/lib-dynamodb` is no longer a peer
329
+ dependency. The store writes and reads attribute values itself, in the shape
330
+ the document client wrote, so existing tables read unchanged; without a
331
+ client it shares the per-region default client with the other stores. Pass
332
+ the `DynamoDBClient` a document client was built from.
333
+ - **Breaking: `checkApiRefusals` fails on a handler it could not check**, a
334
+ finding of the new kind `unchecked`, where it was printed and passed. The
335
+ line says to declare the vocabulary on that init or give the guard its
336
+ codes; `requireTypedRefuse: false` lists such handlers without failing. A
337
+ guard with an explicit `refusals: []` is checked against no codes.
338
+ - **Breaking (mock): the MSW adapter's bookkeeping is off `LambderMockApp`.**
339
+ `hasRegisteredEntry`, `notePassthrough` and `mirrorCookiesIntoDocument` sit
340
+ behind a door only the package's adapter names, `adoptCookieJar` is gone, and
341
+ `lambderMockMswHandler` takes a `LambderMockApp` and refuses anything else.
342
+ - **Breaking (mock): a mock guard declares exactly the refusal codes the
343
+ server's guard of its name declares in `guardDeclarations`**: a different
344
+ list is a compile error at the `guards` option, naming the codes, and
345
+ `create()` throws for one the compiler could not compare.
346
+ - **Breaking (testing): `lambderTestApp` swaps the Lambder classes an app
347
+ builds itself**, built before the test app or after it. Each
348
+ `LambderDdbCache`, `LambderDdbOneShotSecretStore` and `LambderS3UploadBucket`
349
+ answers from an in-memory copy built with its own settings, and each
350
+ `LambderInvokeCaller` built without its own transport is answered by the
351
+ mock app the new `invokeMocks` option gives for its function; with none,
352
+ every call fails naming the function, so a test never reaches AWS without a
353
+ hand-written fake. `reset()` empties the copies.
354
+ - **Breaking (testing): a visitor's plain call returns the output and throws
355
+ on any failure**, naming the endpoint and describing what came back as
356
+ `assertApiSuccess` does, the app's crash included; it returned `undefined`,
357
+ which surfaced later as a read of `undefined`. `.outcome()` is for a test
358
+ that expects a failure.
359
+ - **`generateApiFiles` prints each contract in a Node process of its own**,
360
+ with a heap of `contractHeapMegabytes` (default 8192) and none of the
361
+ script's command-line flags, so one call serves a large app whatever loader
362
+ its script runs under. A process that runs out of heap fails its contract
363
+ and says to raise the option. New type: `LambderApiFilesOptions`.
364
+
365
+ ### Fixed
366
+
367
+ - **A JSON body that is not an object no longer crashes an API call.** A body
368
+ of `5`, `"x"`, `true`, `null`, an array, or no JSON at all is answered with
369
+ the `lambder/invalid-request-payload` refusal (400), the same way by the
370
+ server and by every mock adapter that reads a raw body (the MSW handler and
371
+ the mock invoke transport). The server crashed on it; the mock answered 422,
372
+ or the MSW handler passed a non-JSON body on to the network. The rule lives
373
+ once in the API core: `readApiEnvelope` flags such a body on the request as
374
+ `invalidEnvelope`, and the pipeline's `prepare()` refuses it before the
375
+ version floor, the signature gate or anything else reads the call. On the
376
+ server `ctx.post` is always a plain object: a JSON body that is not an
377
+ object leaves it empty, a body that is not JSON fills it with its form
378
+ fields, and either stays readable as `ctx.rawBody`.
379
+ - **With `apiPath: "/"`, a request to the site root walks the fallback
380
+ chain** (public files, the index shell, the route fallback) rather than
381
+ getting the API not-found envelope; only an actual API call is the API's. A
382
+ non-root `apiPath` is unchanged.
383
+ - **The framework's plain-text 500 for a crashed route carries
384
+ `Content-Type: text/plain; charset=utf-8`**, as its 404 does.
385
+ - **The call summary records the code of a `sessionExpired` refusal** raised
386
+ with a declared code, as it does for `notAuthorized`.
387
+ - **The compile-time checks for a group name given twice and an action
388
+ declared by two parts of a group evaluate as loops**, so an app of many
389
+ small groups (150 or more) compiles; they hit TypeScript's instantiation
390
+ depth limit.
391
+ - **`LambderDdbSessionStore` caps session data at 32 MB of JSON and refuses
392
+ data too large for a DynamoDB item**, on create and update, before writing,
393
+ with an error naming the size and the limit, in place of DynamoDB's raw
394
+ `ValidationException` (a 500 at sign-in with nothing to act on). A stored
395
+ record declaring more than 32 MB reads as no session and is never
396
+ decompressed. Existing records read as before; the session, idempotency and
397
+ cache stores share one implementation of the compressed-or-plain attribute.
398
+ - **`regenerateSession()` compares stored data with stored data.** It
399
+ compared the request's schema output with the record as stored, so with a
400
+ schema that strips keys every rotation saw a change that never happened,
401
+ wrote it back and handed the request unstripped data, and with a schema
402
+ that transforms it threw. It reads the stored record first (one more store
403
+ read per rotation), carries it over as stored, and gives the request the
404
+ schema's output.
405
+ - **`LambderInvokeCaller` leaves the key of a call whose signal had already
406
+ aborted before it was sent unused**, as `LambderCaller` does and as it
407
+ already did for an event over the invoke cap: a refusal after it moves the
408
+ scope on to a new key.
409
+ - **`stopWaitingWhenAborted` rejects at once for a signal that had already
410
+ aborted**, and still handles a later rejection of the work.
411
+ - **A call's timeout firing after its signal had already aborted it no longer
412
+ reports the call as `timeout`.**
413
+ - **The conformance suites' clock starts at 2100-01-01**, so a store whose
414
+ database deletes expired rows itself (a DynamoDB TTL, Redis `EXPIREAT`) runs
415
+ the suites with that deletion on.
416
+ - **`LambderS3FileSource` does not remember a failed `@aws-sdk/client-s3`
417
+ load**: a read after the package is installed in the same process succeeds,
418
+ as it does for the other stores and buckets.
419
+ - **`checkApiRefusals` recognizes a raise by the refuse function the call
420
+ resolves to**, including the forms written out in installed declaration
421
+ files. A refuse reached under a renamed import, a variable, a destructured
422
+ name or a typed parameter is checked; it was skipped, so an undeclared code
423
+ passed and a declared one read as unused. A project that passed may now
424
+ report findings.
425
+ - **An injected `sessionExpired`, or an injected refusal whose code is
426
+ declared `sessionExpired`, ends the session the call carries**, as on the
427
+ server. The next call was still signed in.
428
+ - **The mock's `reset()` expires cookies mirrored into `document.cookie` at
429
+ the Domain and Path each was set with**; a cookie set under a cookie domain
430
+ survived it.
431
+ - **Docs:** the templating and frontend-hosting examples work as raw files in
432
+ a development browser (the title through `htmlVirtualSlots`, the if/else
433
+ example without two `<body>` tags); the mock docs say refusal data is parsed
434
+ under `declareRefusals` and that the server's hooks do not run in the mock;
435
+ the testing docs name the one-shot secret store and the upload bucket among
436
+ the classes an app swaps itself; `docs/invoke.md`'s reporter example
437
+ compiles; the i18n docs import from `lambder/client`; `LambderBackoffTimer`
438
+ is documented with the client; the `initLambder` documentation is one block
439
+ on the declaration, so editors show it on hover.
440
+
12
441
  ## [14.1.1] - 2026-09-30
13
442
 
14
443
  A minor: a refusal code can declare the `sessionExpired` flag, and
package/README.md CHANGED
@@ -106,7 +106,7 @@ npm install lambder zod
106
106
  ```
107
107
 
108
108
  `zod` is a required peer dependency: the published declarations name its types,
109
- so npm installs it alongside lambder. The four AWS SDK clients and `msw` are
109
+ so npm installs it alongside lambder. The AWS SDK packages and `msw` are
110
110
  optional peers, so installing lambder never drags them into your tree. Add
111
111
  whatever the code you actually import needs:
112
112
 
@@ -114,7 +114,7 @@ whatever the code you actually import needs:
114
114
  | --- | --- |
115
115
  | `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 |
116
116
  | `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) |
117
- | `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 |
117
+ | `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb` when sessions or the DynamoDB stores are used; it is loaded on the first table access, so an app that uses neither does not need it |
118
118
  | `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
119
119
  | `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
120
120
  | `lambder/mock` | nothing; `msw` only for the optional network-panel adapter |
@@ -140,7 +140,7 @@ The package ships five entry points; pick by where the code runs:
140
140
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `LambderUploadRunner`, `LambderBackoffTimer`, `LambderSignedClaims`, `html`/`xml` tagged templates, `createLambderI18n` |
141
141
  | `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 |
142
142
  | `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; the store conformance suites, to hold a store you write to the rules Lambder's own meet |
143
- | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call |
143
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call; `checkApiRefusals`: every refusal a handler can reach, held to the codes its endpoint may send |
144
144
 
145
145
  Frontends and shared isomorphic packages should import from `lambder/client`
146
146
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -158,7 +158,7 @@ and the helpers the two caches share), `src/client/`,
158
158
  transport), `src/mock/` (the mock runtime), `src/testing/` (the test app and the store conformance suites),
159
159
  `src/build/` (what a generator script runs at build time), and `src/shared/`
160
160
  (isomorphic modules every entry re-exports, grouped into `wire/` for the
161
- format both sides speak, `contracts/` for the six store and source
161
+ format both sides speak, `contracts/` for the seven store and source
162
162
  interfaces, `transport/` for the caller-to-server seam, and `util/` for
163
163
  helpers).
164
164
  Directories are layers and imports only ever point down;
@@ -209,19 +209,16 @@ framework:
209
209
  ## Versioning and changes
210
210
 
211
211
  Released versions and what each one changed are in
212
- [CHANGELOG.md](./CHANGELOG.md). The current major is v10, which makes both
213
- sides of an answer exact. A success is only ever the handler's parsed output,
214
- and an output is an object or an array, so `caller.api()` is truthy exactly
215
- on success and a success's payload is typed as exactly the output; a hook, a
216
- fallback or an error handler answers an API call with `res.apiRefusal()`,
217
- never with a payload. A refusal names a code the app declared, which the
218
- contract carries and a caller narrows on, with its data typed. Every break
219
- and what to do about it is in the 10.0.1 entry, and the compiler finds most
220
- of them. The ones it cannot are changes of behavior: a refusal whose code its
221
- API does not declare is now a crash rather than an answer, and a success
222
- whose payload is not an object or an array now reads as a server failure. An
223
- app still on v8 goes through the 9.0.1 entry first, and one on an older
224
- major through the entries before it.
212
+ [CHANGELOG.md](./CHANGELOG.md). Both sides of an answer are exact. A success
213
+ is only ever the handler's parsed output, and an output is an object or an
214
+ array, so `caller.api()` is truthy exactly on success and a success's payload
215
+ is typed as exactly the output; a hook, a fallback or an error handler
216
+ answers an API call with `res.apiRefusal()`, never with a payload. A refusal
217
+ names a code the app declared, which the contract carries and a caller
218
+ narrows on, with its data typed. Each major's first entry lists every break
219
+ it made and what to do about it, and the compiler finds most of them; the
220
+ entry names the ones it cannot, which are changes of behavior. An app on an
221
+ older major goes through each major's entry in turn.
225
222
 
226
223
  ## Contributing
227
224
 
@@ -139,10 +139,16 @@ export type LambderContractOfGroups<TGroups extends readonly unknown[]> = Lambde
139
139
  type LambderGroupTypedAny<TGroup> = 0 extends 1 & TGroup ? true : 0 extends 1 & LambderLoadedGroupOf<TGroup> ? true : LambderLoadedGroupOf<TGroup> extends {
140
140
  readonly apis: infer TApis;
141
141
  } ? (0 extends 1 & TApis ? true : string extends keyof TApis ? true : false) : false;
142
- /** The group names one registerApiGroups() call names twice. */
143
- type LambderRepeatedGroupNames<TGroups extends readonly unknown[], TSeen = never> = TGroups extends readonly [infer THead, ...infer TRest] ? (THead extends {
142
+ /**
143
+ * The group names one registerApiGroups() call names twice. Written as an
144
+ * accumulator, the recursive reference standing alone in its branch, so the
145
+ * compiler evaluates it as a loop: an app registers a group per feature,
146
+ * and recursion nested in a union runs into the compiler's instantiation
147
+ * depth limit at a group count a large app reaches.
148
+ */
149
+ type LambderRepeatedGroupNames<TGroups extends readonly unknown[], TSeen = never, TRepeated = never> = TGroups extends readonly [infer THead, ...infer TRest] ? THead extends {
144
150
  name: infer TName;
145
- } ? ([TName] extends [TSeen] ? TName : never) | LambderRepeatedGroupNames<TRest, TSeen | TName> : never) : never;
151
+ } ? LambderRepeatedGroupNames<TRest, TSeen | TName, TRepeated | ([TName] extends [TSeen] ? TName : never)> : TRepeated : TRepeated;
146
152
  /**
147
153
  * What registerApiGroups() asks of its receiver: the instance itself, unless
148
154
  * a group, what a lazy group loads or a group's endpoints is typed `any` (a
@@ -163,8 +169,8 @@ export type LambderRegisteredGroupsCheck<TGroups extends readonly unknown[], TSe
163
169
  export type LambderGroupNameCheck<TName extends string> = TName extends LambderReservedGroupName ? {
164
170
  readonly "lambder: a caller already has a member of this name, so no group may take it": TName;
165
171
  } : unknown;
166
- /** The actions more than one of a group's parts declare. */
167
- type LambderRepeatedActions<TParts extends readonly unknown[], TSeen = never> = TParts extends readonly [infer THead, ...infer TRest] ? (Extract<keyof THead, TSeen> | LambderRepeatedActions<TRest, TSeen | keyof THead>) : never;
172
+ /** The actions more than one of a group's parts declare; an accumulator, as LambderRepeatedGroupNames is, for the same reason. */
173
+ type LambderRepeatedActions<TParts extends readonly unknown[], TSeen = never, TRepeated = never> = TParts extends readonly [infer THead, ...infer TRest] ? LambderRepeatedActions<TRest, TSeen | keyof THead, TRepeated | Extract<keyof THead, TSeen>> : TRepeated;
168
174
  /** A group's parts held to declaring each action once: a part would otherwise replace another's endpoint of the same name. */
169
175
  export type LambderGroupPartsCheck<TParts extends readonly unknown[]> = [
170
176
  LambderRepeatedActions<TParts>
@@ -6,7 +6,8 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
6
6
  * One endpoint's declaration as the pipeline runs it: what the server's
7
7
  * defineApi options carry, minus the handler, in a shape the mock
8
8
  * runtime can restate from a type-only contract. The schemas are optional
9
- * because the mock has none; when input is present, validation runs and the
9
+ * because a mock may have none (its input schema is the generated table's,
10
+ * an entry's own, or neither); when input is present, validation runs and the
10
11
  * handler sees the parsed payload. Output is part of the endpoint's
11
12
  * signature (apiSignatureOf), which is what a client's build is checked
12
13
  * against; the server also parses every output a handler returns through it
@@ -21,8 +21,8 @@ export declare const refusalEnvelope: (apiVersion: string | null | undefined, {
21
21
  * server, onInvalidInput in the mock) as its envelope, once the config is
22
22
  * known to be a refusal: a refusal message or one of the three flags, so a
23
23
  * reader never takes it for the handler's output, and a message with a
24
- * framework code or none and no data, since it answers outside any one
25
- * endpoint's declared refusals. `writer` names the call in the error, and
24
+ * framework code that carries no data, or none, since it answers outside any
25
+ * one endpoint's declared refusals. `writer` names the call in the error, and
26
26
  * the logList is the config's own or the one given.
27
27
  */
28
28
  export declare const plainRefusalEnvelope: (apiVersion: string | null | undefined, config: LambderApiRefusalConfig, writer: string, logList?: unknown[]) => LambderApiRefusalEnvelope;
@@ -46,8 +46,8 @@ export const refusalEnvelope = (apiVersion, { versionExpired, sessionExpired, no
46
46
  * server, onInvalidInput in the mock) as its envelope, once the config is
47
47
  * known to be a refusal: a refusal message or one of the three flags, so a
48
48
  * reader never takes it for the handler's output, and a message with a
49
- * framework code or none and no data, since it answers outside any one
50
- * endpoint's declared refusals. `writer` names the call in the error, and
49
+ * framework code that carries no data, or none, since it answers outside any
50
+ * one endpoint's declared refusals. `writer` names the call in the error, and
51
51
  * the logList is the config's own or the one given.
52
52
  */
53
53
  export const plainRefusalEnvelope = (apiVersion, config, writer, logList) => {
@@ -60,6 +60,12 @@ export const plainRefusalEnvelope = (apiVersion, config, writer, logList) => {
60
60
  if (message.code !== undefined && !isLambderRefusalCode(message.code)) {
61
61
  throw new Error(`Lambder: ${writer} was given the code "${message.code}". It answers outside any one API's declared refusals, so its message carries a framework code or none.`);
62
62
  }
63
+ // The one framework code that carries data names the policy that
64
+ // refused, which only the rate-limit engine knows, so a rate limit
65
+ // is answered by charging the policy (ctx.rateLimit), never by hand.
66
+ if (message.code === LAMBDER_REFUSAL_CODES.rateLimited) {
67
+ throw new Error(`Lambder: ${writer} was given the code "${message.code}". A rate limit refuses with the policy that refused and the seconds to wait as its data; charge the policy with ctx.rateLimit, which refuses the call itself.`);
68
+ }
63
69
  if (message.data !== undefined) {
64
70
  throw new Error(`Lambder: ${writer} was given a message with data. Only a code an API declares carries data, and this answer is outside any one API's declaration.`);
65
71
  }