lambder 10.2.1 → 11.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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,58 @@ 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
+ ## [11.0.1] - 2026-09-29
13
+
14
+ A major for an app with many APIs: what one API costs the type checker no
15
+ longer grows with the app's keyed guards and rate-limit policies. The same
16
+ declarations compile and the same ones fail, and nothing changes at runtime.
17
+ It is a major because the exported option types no longer take the API's
18
+ input.
19
+
20
+ ### Changed (breaking)
21
+
22
+ - **`LambderGuardsOption`, `LambderRateLimitOption`, `LambderAllowedGuardNames`,
23
+ `LambderParamlessGuardNames` and `LambderAllowedPolicyNames` no longer take
24
+ a payload.** They name what an instance lets one kind of API declare
25
+ (session guards and session-keyed policies only on session APIs) and depend
26
+ on the instance alone. Whether an API's input carries the fields an apiInput
27
+ guard or an apiInput-keyed policy reads is asked separately, of the names
28
+ the API declared, by `LambderGuardNamesInputLacks` and
29
+ `LambderPolicyNamesInputLacks`.
30
+ - To move: drop the payload argument (`LambderGuardsOption<Guards, true>`
31
+ for `LambderGuardsOption<Guards, Payload, true>`), and where you relied on
32
+ the payload filter, ask the new types of the names you declared.
33
+
34
+ ### Changed
35
+
36
+ - **An API's `guards` and `rateLimit` options are held to its input once the
37
+ input is known.** Their constraints used to read the API's own input type,
38
+ so for every API the compiler rebuilt the names it may declare, over every
39
+ guard and policy the instance holds, while that input was still being
40
+ inferred. The work grew with an app's APIs times its apiInput guards and
41
+ apiInput-keyed policies, exponentially in the worst case: 25 APIs
42
+ registered in one chain, each declaring one of 10 apiInput guards, took
43
+ 12.6 million type instantiations. The constraints now depend on the
44
+ instance alone, and `LambderPayloadSliceCheck`, intersected onto the
45
+ options, refuses an apiInput guard or policy whose fields the input does
46
+ not carry. The same 25 APIs now take 45 thousand, about what they cost on
47
+ guards that read no input.
48
+ - **Where the error shows.** A guard or policy the input cannot carry is
49
+ refused on the `guards` or `rateLimit` value rather than on the name inside
50
+ it, and the message names the guards or policies at fault ("lambder: these
51
+ guards read fields this API's input does not carry"). Completion inside
52
+ those options offers every declared name.
53
+
54
+ ### Added
55
+
56
+ - **`LambderGuardNamesInputLacks` and `LambderPolicyNamesInputLacks`**: the
57
+ apiInput guards, and the apiInput-keyed policies, among some names whose
58
+ fields an input does not carry. The input is compared whole, so a union
59
+ input carries a slice only when every member does.
60
+ - **A type-cost test** (`tests/api/api-policy-type-cost.test.ts`): forty APIs
61
+ compiled on an instance whose ten guards and ten policies read the input,
62
+ and on one whose same-named ones do not, must cost about the same.
63
+
12
64
  ## [10.2.1] - 2026-09-29
13
65
 
14
66
  A minor for an app that keeps its cache in its own storage. It writes a few
@@ -272,33 +272,40 @@ export type LambderGuardMetaMap<TGuards> = {
272
272
  [K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
273
273
  };
274
274
  /**
275
- * The guard name when the API's payload carries the guard's apiInput slice.
276
- * The payload is compared whole, not member by member: a bare `TPayload
277
- * extends R` would distribute over a union input, so a slice one member
278
- * carries would be allowed on an API whose other members lack it, and every
279
- * request of those members would be refused by the slice's parse.
275
+ * Guard names an API may declare: every guard the instance holds, session
276
+ * guards only on session APIs. Whether the API's input carries the fields an
277
+ * apiInput guard reads is asked of the names it declared, once that input is
278
+ * known, by LambderGuardNamesInputLacks.
280
279
  */
281
- type LambderGuardNameIfPayloadOk<TGuards, K extends keyof TGuards, TPayload> = TGuards[K] extends {
282
- apiInput: infer R;
283
- } ? ([TPayload] extends [R] ? K : never) : K;
284
- /**
285
- * Guard names an API may declare: apiInput-mode guards only when the API's
286
- * payload carries their fields, session guards only on session APIs.
287
- */
288
- export type LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession extends boolean = true> = {
280
+ export type LambderAllowedGuardNames<TGuards, TIncludeSession extends boolean = true> = {
289
281
  [K in keyof TGuards]: TGuards[K] extends {
290
282
  session: true;
291
- } ? (TIncludeSession extends true ? LambderGuardNameIfPayloadOk<TGuards, K, TPayload> : never) : LambderGuardNameIfPayloadOk<TGuards, K, TPayload>;
283
+ } ? (TIncludeSession extends true ? K : never) : K;
292
284
  }[keyof TGuards] & string;
285
+ /**
286
+ * The apiInput guards among `TNames` whose fields the API's payload does not
287
+ * carry. addApi and addSessionApi ask this once the API's input is known, of
288
+ * the names its guards option holds; see LambderPayloadSliceCheck.
289
+ *
290
+ * The payload is compared whole, not member by member: a bare `TPayload
291
+ * extends R` would distribute over a union input, so a slice one member
292
+ * carries would pass on an API whose other members lack it, and every
293
+ * request of those members would be refused by the slice's parse.
294
+ */
295
+ export type LambderGuardNamesInputLacks<TGuards, TNames, TPayload> = {
296
+ [K in TNames & keyof TGuards]: TGuards[K] extends {
297
+ apiInput: infer R;
298
+ } ? ([TPayload] extends [R] ? never : K) : never;
299
+ }[TNames & keyof TGuards] & string;
293
300
  /** The allowed guard names whose handler takes no param (usable in the string/array forms). */
294
- export type LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession extends boolean> = {
295
- [K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]: TGuards[K] extends {
301
+ export type LambderParamlessGuardNames<TGuards, TIncludeSession extends boolean> = {
302
+ [K in LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards]: TGuards[K] extends {
296
303
  param: undefined;
297
304
  } ? K & string : never;
298
- }[LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards];
305
+ }[LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards];
299
306
  /** The map form's full shape: every declarable guard name, each carrying its own param type. */
300
- type LambderGuardsMap<TGuards, TPayload, TIncludeSession extends boolean> = {
301
- readonly [K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]?: TGuards[K] extends {
307
+ type LambderGuardsMap<TGuards, TIncludeSession extends boolean> = {
308
+ readonly [K in LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards]?: TGuards[K] extends {
302
309
  param: undefined;
303
310
  } ? true : TGuards[K] extends {
304
311
  param: infer P;
@@ -312,11 +319,15 @@ type LambderGuardsMap<TGuards, TPayload, TIncludeSession extends boolean> = {
312
319
  *
313
320
  * Every form is non-empty by construction, so declaring the option is always
314
321
  * declaring a guard. See LambderNonEmptyOptionMap.
322
+ *
323
+ * It depends on the instance alone, never on the API's input, so the
324
+ * compiler builds it once per instance rather than once per API; see
325
+ * LambderPayloadSliceCheck for why, and for where the input is asked.
315
326
  */
316
- export type LambderGuardsOption<TGuards, TPayload, TIncludeSession extends boolean> = LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession> | readonly [
317
- LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession>,
318
- ...LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession>[]
319
- ] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards, TPayload, TIncludeSession>>;
327
+ export type LambderGuardsOption<TGuards, TIncludeSession extends boolean> = LambderParamlessGuardNames<TGuards, TIncludeSession> | readonly [
328
+ LambderParamlessGuardNames<TGuards, TIncludeSession>,
329
+ ...LambderParamlessGuardNames<TGuards, TIncludeSession>[]
330
+ ] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards, TIncludeSession>>;
320
331
  /**
321
332
  * The typed ctx.guardData an API's handler sees: declared guards that return
322
333
  * a value, keyed by name. Check-only (void) guards never appear.
@@ -147,22 +147,35 @@ export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderA
147
147
  type LambderPolicyPerOf<TPolicy> = "per" extends keyof TPolicy ? TPolicy["per" & keyof TPolicy] : undefined;
148
148
  /**
149
149
  * Policy names an API may reference: session-keyed policies only on session
150
- * APIs, apiInput-keyed policies only when the API's payload carries the
151
- * key's fields, and never a policy without `per`, whose key only the code
152
- * that charges it knows. A policy whose type does not settle its `per` is
153
- * allowed here and checked at registration.
154
- *
155
- * The payload is compared whole, as the guards' check does it: a union input
156
- * one of whose members lacks the key's fields does not carry them, and every
157
- * request of that member would be refused by the key slice's parse.
150
+ * APIs, and never a policy without `per`, whose key only the code that
151
+ * charges it knows. A policy whose type does not settle its `per` is allowed
152
+ * here and checked at registration. Whether the API's input carries the
153
+ * fields an apiInput-keyed policy reads is asked of the names it declared,
154
+ * once that input is known, by LambderPolicyNamesInputLacks.
158
155
  */
159
- export type LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession extends boolean> = {
156
+ export type LambderAllowedPolicyNames<TPolicies, TIncludeSession extends boolean> = {
160
157
  [K in keyof TPolicies]: [
161
158
  LambderPolicyPerOf<TPolicies[K]>
162
- ] extends [undefined] ? never : [LambderPolicyPerOf<TPolicies[K]>] extends ["session"] ? (TIncludeSession extends true ? K : never) : [LambderPolicyPerOf<TPolicies[K]>] extends [{
163
- apiInput: infer S extends z.ZodType;
164
- }] ? ([TPayload] extends [z.input<S>] ? K : never) : K;
159
+ ] extends [undefined] ? never : [LambderPolicyPerOf<TPolicies[K]>] extends ["session"] ? (TIncludeSession extends true ? K : never) : K;
165
160
  }[keyof TPolicies] & string;
161
+ /**
162
+ * The apiInput-keyed policies among `TNames` whose key fields the API's
163
+ * payload does not carry. addApi and addSessionApi ask this once the API's
164
+ * input is known, of the names its rateLimit option holds; see
165
+ * LambderPayloadSliceCheck.
166
+ *
167
+ * The payload is compared whole, as LambderGuardNamesInputLacks compares it:
168
+ * a union input one of whose members lacks the key's fields does not carry
169
+ * them, and every request of that member would be refused by the key
170
+ * slice's parse.
171
+ */
172
+ export type LambderPolicyNamesInputLacks<TPolicies, TNames, TPayload> = {
173
+ [K in TNames & keyof TPolicies]: [
174
+ LambderPolicyPerOf<TPolicies[K]>
175
+ ] extends [{
176
+ apiInput: infer S extends z.ZodType;
177
+ }] ? ([TPayload] extends [z.input<S>] ? never : K) : never;
178
+ }[TNames & keyof TPolicies] & string;
166
179
  /**
167
180
  * Policy names a handler may charge itself: every policy except one keyed by
168
181
  * a `{ apiInput?, handler }` key, which derives its key from an API's payload
@@ -222,8 +235,8 @@ type LambderRateLimitOverrideFor<TPolicy> = TPolicy extends {
222
235
  budget: "perPolicy";
223
236
  } ? Pick<LambderRateLimitOverride, "refusal"> : LambderRateLimitOverride;
224
237
  /** The map form's full shape: every referable policy name, each carrying its own override. */
225
- type LambderRateLimitMap<TPolicies, TPayload, TIncludeSession extends boolean> = {
226
- readonly [K in LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
238
+ type LambderRateLimitMap<TPolicies, TIncludeSession extends boolean> = {
239
+ readonly [K in LambderAllowedPolicyNames<TPolicies, TIncludeSession> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
227
240
  };
228
241
  /**
229
242
  * The per-API `rateLimit` option: one policy name, a non-empty ordered list
@@ -234,11 +247,14 @@ type LambderRateLimitMap<TPolicies, TPayload, TIncludeSession extends boolean> =
234
247
  * Every form is non-empty by construction (LambderNonEmptyOptionMap, as the
235
248
  * guards option uses), since `rateLimit: {}`, `rateLimit: []` and
236
249
  * `rateLimit: { policy: undefined }` would announce a limit and enforce none.
250
+ *
251
+ * It depends on the instance alone, never on the API's input, as
252
+ * LambderGuardsOption does; see LambderPayloadSliceCheck.
237
253
  */
238
- export type LambderRateLimitOption<TPolicies, TPayload, TIncludeSession extends boolean> = LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> | readonly [
239
- LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>,
240
- ...LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>[]
241
- ] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies, TPayload, TIncludeSession>>;
254
+ export type LambderRateLimitOption<TPolicies, TIncludeSession extends boolean> = LambderAllowedPolicyNames<TPolicies, TIncludeSession> | readonly [
255
+ LambderAllowedPolicyNames<TPolicies, TIncludeSession>,
256
+ ...LambderAllowedPolicyNames<TPolicies, TIncludeSession>[]
257
+ ] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies, TIncludeSession>>;
242
258
  /**
243
259
  * When in a call a policy is checked.
244
260
  *
@@ -24,7 +24,7 @@ import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.j
24
24
  import type { LambderContractEntry, LambderJsonOf, LambderMergeContract } from "../shared/wire/LambderApiContract.js";
25
25
  import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
26
26
  import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
27
- import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderObjectOutputCheck, type LambderRequirableGuardsField, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
27
+ import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderObjectOutputCheck, type LambderPayloadSliceCheck, type LambderRequirableGuardsField, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
28
28
  /** Everything `lambder/testing` may put under a built instance: the pipeline's stores, and the source its files are read from. */
29
29
  export type LambderInstanceBackends = LambderPipelineBackends & {
30
30
  fileSource?: LambderFileSource;
@@ -167,7 +167,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
167
167
  addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
168
168
  addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TSessionData, LambderPathParamsOf<TPath>, {}, _TRateLimitPolicies>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
169
169
  addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TSessionData> & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
170
- addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, false> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
170
+ addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, false> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
171
171
  input: TInput;
172
172
  output: TOutput;
173
173
  } & {
@@ -189,12 +189,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
189
189
  * both ends. A transport setting of this server's, not part of the API's contract.
190
190
  */
191
191
  compress?: boolean | "auto";
192
- } & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt> & LambderObjectOutputCheck<TOutput>,
192
+ } & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<_TGuards, _TRateLimitPolicies, TGuardsOpt, TRateOpt, z.input<TInput>>,
193
193
  /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
194
194
  handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, TSessionData, _TRateLimitPolicies> & {
195
195
  refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>, _TRefusalCodesRequired>;
196
196
  }) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, "public", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled, _TRefusals, _TRefusalCodesRequired>;
197
- addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
197
+ addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
198
198
  input: TInput;
199
199
  output: TOutput;
200
200
  } & {
@@ -216,7 +216,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
216
216
  * both ends. A transport setting of this server's, not part of the API's contract.
217
217
  */
218
218
  compress?: boolean | "auto";
219
- } & LambderRequirableGuardsField<_TSessionGuardsRequired, TGuardsOpt> & LambderSessionEnabledInstance<_TSessionsEnabled> & LambderObjectOutputCheck<TOutput>,
219
+ } & LambderRequirableGuardsField<_TSessionGuardsRequired, TGuardsOpt> & LambderSessionEnabledInstance<_TSessionsEnabled> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<_TGuards, _TRateLimitPolicies, TGuardsOpt, TRateOpt, z.input<TInput>>,
220
220
  /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
221
221
  handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, _TRateLimitPolicies> & {
222
222
  refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>, _TRefusalCodesRequired>;
@@ -11,12 +11,12 @@ import type { LambderSessionDataRefreshConfig } from "../session/LambderSessionM
11
11
  import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
12
12
  import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
13
13
  import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
14
- import type { LambderApiGuard } from "../api/LambderApiGuards.js";
15
- import type { LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig } from "../api/LambderApiRateLimits.js";
14
+ import type { LambderApiGuard, LambderGuardNamesInputLacks } from "../api/LambderApiGuards.js";
15
+ import type { LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderPolicyNamesInputLacks } from "../api/LambderApiRateLimits.js";
16
16
  import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
17
17
  import { type LambderRefusalVocabulary } from "../api/LambderApiRefusals.js";
18
18
  import type { LambderNoExtraKeys } from "../shared/util/LambderTypeUtilities.js";
19
- import type { LambderJsonOf } from "../shared/wire/LambderApiContract.js";
19
+ import type { LambderGuardNamesIn, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
20
20
  import type { LambderRateLimitMessage } from "../shared/wire/LambderApiOptionValues.js";
21
21
  import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
22
22
  export type LambderRouteHandler = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
@@ -385,6 +385,31 @@ type LambderObjectOutputRefusal = {
385
385
  readonly "lambder: an API answers with an object or an array. One with nothing to answer declares output: z.object({}) and returns {}.": never;
386
386
  };
387
387
  };
388
+ /**
389
+ * An API's guards and rate limits held to its input once the input is known.
390
+ * Intersected onto an API's options: a guard or a policy that reads fields
391
+ * the input does not carry is refused on the option that names it, the
392
+ * property name is the message and its value the names at fault.
393
+ *
394
+ * The options' own constraints admit every name the instance declares, and
395
+ * the input is asked about here instead, on purpose. A constraint that reads
396
+ * the API's input is rebuilt for every API while that input is still being
397
+ * inferred, over every name the instance declares, so the compiler's work
398
+ * grows with an app's APIs times its keyed guards and policies, exponentially
399
+ * in the worst case. Here the question is asked once per API, of the names
400
+ * that API declared.
401
+ */
402
+ export type LambderPayloadSliceCheck<TGuards, TPolicies, TGuardsOpt, TRateOpt, TPayload> = LambderGuardsSliceCheck<LambderGuardNamesInputLacks<TGuards, LambderGuardNamesIn<TGuardsOpt>, TPayload>> & LambderRateLimitSliceCheck<LambderPolicyNamesInputLacks<TPolicies, LambderGuardNamesIn<TRateOpt>, TPayload>>;
403
+ type LambderGuardsSliceCheck<TLacking> = [TLacking] extends [never] ? unknown : {
404
+ guards: {
405
+ readonly "lambder: these guards read fields this API's input does not carry": TLacking;
406
+ };
407
+ };
408
+ type LambderRateLimitSliceCheck<TLacking> = [TLacking] extends [never] ? unknown : {
409
+ rateLimit: {
410
+ readonly "lambder: these rate limits are keyed by fields this API's input does not carry": TLacking;
411
+ };
412
+ };
388
413
  /**
389
414
  * The `guards` field of an API's options: optional by default, required once
390
415
  * create() received the require*ApiGuards flag for that kind of API, so that
package/dist/index.d.ts CHANGED
@@ -128,12 +128,12 @@ export type { LambderDdbIdempotencyStoreOptions } from "./stores/LambderDdbIdemp
128
128
  export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
129
129
  export { lambderGuard, lambderRateLimitKey } from "./core/LambderPolicyBuilders.js";
130
130
  export { lambderGuardBuilder } from "./api/LambderApiGuards.js";
131
- export type { LambderGuardBuilder, LambderApiGuard, LambderGuardMeta, LambderGuardMetaMap, LambderGuardRunAt, LambderAllowedGuardNames, LambderParamlessGuardNames, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusals, LambderGuardRefusalNamesOf, } from "./api/LambderApiGuards.js";
131
+ export type { LambderGuardBuilder, LambderApiGuard, LambderGuardMeta, LambderGuardMetaMap, LambderGuardRunAt, LambderAllowedGuardNames, LambderGuardNamesInputLacks, LambderParamlessGuardNames, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusals, LambderGuardRefusalNamesOf, } from "./api/LambderApiGuards.js";
132
132
  export { LambderApiRefusalValidationError } from "./api/LambderApiRefusals.js";
133
133
  export type { LambderRefusalDeclaration, LambderRefusalVocabulary, LambderApiAllowedRefusal, LambderApiAllowedRefusals, LambderEndpointRefusals, LambderRefusalsOption, LambderRefusalNamesIn, LambderHandlerRefusalsOf, LambderWireRefusalsOf, } from "./api/LambderApiRefusals.js";
134
134
  export { lambderRateLimitKeyBuilder, rateLimitRefusal, DEFAULT_RATE_LIMIT_REFUSAL } from "./api/LambderApiRateLimits.js";
135
135
  export type { LambderRateLimitKeyBuilder } from "./api/LambderApiRateLimits.js";
136
- export type { LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult, LambderChargeablePolicyNames, LambderChargeKeyArgs, LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderRateLimitChargeAt, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
136
+ export type { LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult, LambderChargeablePolicyNames, LambderChargeKeyArgs, LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderRateLimitChargeAt, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderPolicyNamesInputLacks, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
137
137
  export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js";
138
138
  export type { LambderGuardsOptionValue, LambderRateLimitMessage, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderRefusalsOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
139
139
  export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "10.2.1",
3
+ "version": "11.0.1",
4
4
  "sideEffects": false,
5
5
  "description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
6
6
  "keywords": [