lambder 14.0.1 → 15.0.1

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