lambder 10.0.1 → 10.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,37 @@ 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
+ ## [10.1.1] - 2026-09-28
13
+
14
+ A minor for an app made of parts. Each part can declare its own guards and
15
+ rate-limit policies beside the APIs that use them, and the instance composes
16
+ the maps at creation.
17
+
18
+ ### Added
19
+
20
+ - **`guards` and `rateLimits.policies` take a list of maps** as well as one
21
+ map. The instance declares every name in the list, so an API names a guard
22
+ or a policy from any map as it would from the only one, and
23
+ `apiSignatures()` and `apiOptionEntries()` read the merged map. A name two
24
+ maps declare is a compile error on the list, whose message names it, and a
25
+ throw at creation, where a spread would have let one replace the other
26
+ without a word. Every map of a list is held to the same surplus-key and
27
+ refusal-code checks as a lone map. The mock runtime's `create()` still
28
+ takes one map of each.
29
+
30
+ ## [10.0.2] - 2026-09-28
31
+
32
+ ### Fixed
33
+
34
+ - **A guard an init's `guard()` builds inside a map held to `LambderApiGuard`**
35
+ (`{ ... } satisfies Record<string, LambderApiGuard<any, any, any>>`, the
36
+ shape an app's guards file takes) did not compile when it named no
37
+ `refusals`: the map's contextual type widened the guard's refusals to
38
+ `readonly string[]`, which the vocabulary check read as codes it does not
39
+ hold, so every such guard was asked for a `refusals` option. The refusals in
40
+ a built guard's type are no longer an inference site, so a guard that names
41
+ none declares none wherever it is built.
42
+
12
43
  ## [10.0.1] - 2026-09-28
13
44
 
14
45
  A major that makes both sides of an API answer exact. A success is only ever
@@ -168,7 +168,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
168
168
  session: true;
169
169
  handler: (ctx: TSessionCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
170
170
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
171
- refusals?: TRefusals;
171
+ refusals?: NoInfer<TRefusals>;
172
172
  apiInput: TInput;
173
173
  guardInput?: undefined;
174
174
  session: true;
@@ -178,7 +178,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
178
178
  apiInput: TInput;
179
179
  handler: (ctx: TCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
180
180
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
181
- refusals?: TRefusals;
181
+ refusals?: NoInfer<TRefusals>;
182
182
  apiInput: TInput;
183
183
  guardInput?: undefined;
184
184
  session?: undefined;
@@ -189,7 +189,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
189
189
  session: true;
190
190
  handler: (ctx: TSessionCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
191
191
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
192
- refusals?: TRefusals;
192
+ refusals?: NoInfer<TRefusals>;
193
193
  guardInput: TInput;
194
194
  apiInput?: undefined;
195
195
  session: true;
@@ -199,7 +199,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
199
199
  guardInput: TInput;
200
200
  handler: (ctx: TCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
201
201
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
202
- refusals?: TRefusals;
202
+ refusals?: NoInfer<TRefusals>;
203
203
  guardInput: TInput;
204
204
  apiInput?: undefined;
205
205
  session?: undefined;
@@ -209,7 +209,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
209
209
  session: true;
210
210
  handler: (ctx: TSessionCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
211
211
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
212
- refusals?: TRefusals;
212
+ refusals?: NoInfer<TRefusals>;
213
213
  apiInput?: undefined;
214
214
  guardInput?: undefined;
215
215
  session: true;
@@ -218,7 +218,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
218
218
  <TParam = undefined, TOutput = void, const TRefusals extends readonly string[] = readonly []>(guard: {
219
219
  handler: (ctx: TCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
220
220
  } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
221
- refusals?: TRefusals;
221
+ refusals?: NoInfer<TRefusals>;
222
222
  apiInput?: undefined;
223
223
  guardInput?: undefined;
224
224
  session?: undefined;
@@ -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 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 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;
@@ -96,7 +96,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
96
96
  private readonly pipeline;
97
97
  /** Every registered API by name: the duplicate-name check, and what apiSignatures() digests. */
98
98
  private readonly apiDefinitions;
99
- /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
99
+ /** The guards given at creation, merged into one map, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
100
100
  private readonly guards;
101
101
  /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
102
102
  private readonly rateLimitPolicies;
@@ -129,7 +129,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
129
129
  private readonly trustedClientIpHeaders;
130
130
  private readonly trustedHostHeaders;
131
131
  private requirePublicApiGuards;
132
- constructor(options?: LambderCreateOptions<TSessionData>);
132
+ constructor(given?: LambderCreateOptions<TSessionData>);
133
133
  setRouteFallbackHandler(routeFallbackHandler: LambderFallbackHandler): this;
134
134
  setApiFallbackHandler(apiFallbackHandler: LambderFallbackHandler): this;
135
135
  setApiInputValidationErrorHandler(apiInputValidationErrorHandler: LambderInputValidationHandler): this;
@@ -518,9 +518,9 @@ export declare const initLambder: <TSessionData = any>() => {
518
518
  * endpoint's codes alone.
519
519
  */
520
520
  refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TRefusals, keyof TRefusals & string>, TRequireCodes>;
521
- create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "rateLimits" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, TRefusals>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
522
- policies: infer TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>;
523
- } ? TPolicies : {}, TOptions["guards"] extends Record<string, LambderApiGuard<any, any, any>> ? LambderGuardMetaMap<TOptions["guards"]> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, TRefusals, TRequireCodes>;
521
+ create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, TRefusals>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
522
+ policies: infer TPolicies;
523
+ } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {}, TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, TRefusals, TRequireCodes>;
524
524
  guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, TRefusals, TRequireCodes>;
525
525
  rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
526
526
  };
@@ -544,9 +544,9 @@ export declare const initLambder: <TSessionData = any>() => {
544
544
  code?: undefined;
545
545
  data?: undefined;
546
546
  }) | undefined) => never;
547
- create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "rateLimits" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, {}>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
548
- policies: infer TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>;
549
- } ? TPolicies : {}, TOptions["guards"] extends Record<string, LambderApiGuard<any, any, any>> ? LambderGuardMetaMap<TOptions["guards"]> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, {}, false>;
547
+ create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, {}>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
548
+ policies: infer TPolicies;
549
+ } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {}, TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, {}, false>;
550
550
  guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, {}, false>;
551
551
  rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
552
552
  };
@@ -27,7 +27,7 @@ import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PA
27
27
  import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
28
28
  import { LambderCrashHandling } from "./LambderCrashHandling.js";
29
29
  import { policyBuildersFor } from "./LambderPolicyBuilders.js";
30
- import { assertCreateOptions, } from "./LambderCreateOptions.js";
30
+ import { assertCreateOptions, mergeNamedMaps, } from "./LambderCreateOptions.js";
31
31
  /** The refusals of a name no API is registered under: no code, and none required. */
32
32
  const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
33
33
  /**
@@ -88,7 +88,7 @@ export default class Lambder {
88
88
  pipeline;
89
89
  /** Every registered API by name: the duplicate-name check, and what apiSignatures() digests. */
90
90
  apiDefinitions = new Map();
91
- /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
91
+ /** The guards given at creation, merged into one map, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
92
92
  guards;
93
93
  /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
94
94
  rateLimitPolicies;
@@ -121,7 +121,15 @@ export default class Lambder {
121
121
  trustedClientIpHeaders;
122
122
  trustedHostHeaders;
123
123
  requirePublicApiGuards;
124
- constructor(options = {}) {
124
+ constructor(given = {}) {
125
+ // The guards and the rate-limit policies as one map each, whether
126
+ // they were given as one or as a list; a name two maps declare
127
+ // throws here.
128
+ const options = {
129
+ ...given,
130
+ guards: given.guards && mergeNamedMaps(given.guards, "guard"),
131
+ rateLimits: given.rateLimits && { ...given.rateLimits, policies: mergeNamedMaps(given.rateLimits.policies, "rate-limit policy") },
132
+ };
125
133
  assertCreateOptions(options);
126
134
  this.files = options.files ? new LambderFiles(options.files) : null;
127
135
  this.apiPath = options.apiPath ?? DEFAULT_API_PATH;
@@ -112,6 +112,26 @@ export type LambderHandler = {
112
112
  (event: LambderHttpEvent, context: Context): Promise<LambderHttpResponse>;
113
113
  (event: unknown, context: Context): Promise<unknown>;
114
114
  };
115
+ /**
116
+ * One map of named declarations, or a list of them. An app made of parts,
117
+ * each declaring its own guards or rate-limit policies beside its APIs, hands
118
+ * create() the list, and the instance declares every name in it. A name two
119
+ * maps declare is refused rather than one quietly replacing the other.
120
+ */
121
+ export type LambderNamedMapsOption<TMap> = TMap | readonly TMap[];
122
+ /** The one map a named-maps option declares: a list's members intersected, a lone map as it is. */
123
+ export type LambderMergedNamedMaps<T> = T extends readonly [] ? {} : T extends readonly [infer TFirst, ...infer TRest] ? TFirst & LambderMergedNamedMaps<TRest> : T extends readonly (infer TMember)[] ? TMember : T;
124
+ /** The names a list declares in more than one of its maps: never for a lone map, or for a list whose members are not known one by one. */
125
+ type LambderRepeatedNames<T, TSeen extends PropertyKey = never> = T extends readonly [infer TFirst, ...infer TRest] ? (keyof TFirst & TSeen) | LambderRepeatedNames<TRest, TSeen | keyof TFirst> : never;
126
+ /** Intersected onto a list: a name declared in two of its maps is a compile error, and the property name is the message. */
127
+ type LambderNoRepeatedNames<T> = [LambderRepeatedNames<T>] extends [never] ? unknown : {
128
+ readonly "lambder: a name is declared in more than one of these maps. Declare each guard and each rate-limit policy once.": LambderRepeatedNames<T>;
129
+ };
130
+ /**
131
+ * A named-maps option as one map: a lone map as it is, a list merged in
132
+ * order. A name declared in two maps of the list is refused.
133
+ */
134
+ export declare const mergeNamedMaps: <TMap extends Record<string, unknown>>(option: LambderNamedMapsOption<TMap>, what: "guard" | "rate-limit policy") => TMap;
115
135
  /** Session configuration (the `session` option of create/new): where sessions rest, and how their cookies are scoped. */
116
136
  export type LambderSessionOptions<TSessionData = any> = {
117
137
  /**
@@ -147,6 +167,20 @@ export type LambderSessionOptions<TSessionData = any> = {
147
167
  /** Hashing and randomness for the session tokens. Default: WebCrypto. */
148
168
  crypto?: LambderSessionCrypto;
149
169
  };
170
+ /** One map of the server's guards, each pinned to the render contexts of the app's session type. */
171
+ export type LambderGuardsMap<TSessionData = any> = Record<string, LambderApiGuard<any, any, any, LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>>>;
172
+ /** One map of the server's rate-limit policies. */
173
+ export type LambderRateLimitPoliciesMap = Record<string, LambderApiRateLimitPolicyConfig<LambderRenderContext>>;
174
+ /** The `rateLimits` option of create(): the engine's config, with `policies` as one map or a list of maps. */
175
+ export type LambderCreateRateLimitsOption = Omit<LambderApiRateLimitsConfig<LambderRateLimitPoliciesMap>, "policies"> & {
176
+ /** Named policies referenced (typed) from addApi/addSessionApi: one map, or a list of maps merged in order. */
177
+ policies: LambderNamedMapsOption<LambderRateLimitPoliciesMap>;
178
+ };
179
+ /** The options as the instance holds them: the guards and the rate-limit policies merged into one map each. */
180
+ export type LambderMergedCreateOptions<TSessionData = any> = Omit<LambderCreateOptions<TSessionData>, "guards" | "rateLimits"> & {
181
+ guards?: LambderGuardsMap<TSessionData>;
182
+ rateLimits?: LambderApiRateLimitsConfig<LambderRateLimitPoliciesMap>;
183
+ };
150
184
  /**
151
185
  * Everything an instance is configured with, in ONE declaration: base
152
186
  * serving options plus the type-affecting policy layer (rate limits, guards,
@@ -244,16 +278,22 @@ export type LambderCreateOptions<TSessionData = any> = {
244
278
  cors?: boolean | LambderCorsConfig;
245
279
  /** Sessions over a store of your choosing; required for addSessionApi/addSessionRoute. */
246
280
  session?: LambderSessionOptions<TSessionData>;
247
- /** Declarative per-API rate limiting: your limiter plus named policies APIs reference (typed) via the `rateLimit` option. */
248
- rateLimits?: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig<LambderRenderContext>>>;
281
+ /**
282
+ * Declarative per-API rate limiting: your limiter plus named policies APIs
283
+ * reference (typed) via the `rateLimit` option. `policies` is one map, or
284
+ * a list of maps (one per part of the app) that the instance merges.
285
+ */
286
+ rateLimits?: LambderCreateRateLimitsOption;
249
287
  /**
250
288
  * Named guards APIs reference (typed) via the `guards` option; build each
251
289
  * with `initLambder<SessionData>().guard()`, whose handlers see the app's
252
290
  * session type, or lambderGuard(). Pinned to the render contexts, so a
253
291
  * guard built for another adapter, or for another session type, is
254
292
  * rejected here rather than reading fields that are not on its context.
293
+ * One map, or a list of maps (one per part of the app) that the instance
294
+ * merges.
255
295
  */
256
- guards?: Record<string, LambderApiGuard<any, any, any, LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>>>;
296
+ guards?: LambderNamedMapsOption<LambderGuardsMap<TSessionData>>;
257
297
  /**
258
298
  * The app's refusal vocabulary: every code an API or a guard may refuse
259
299
  * with, each declared once with the schema of its data (`{ data: schema }`
@@ -386,6 +426,22 @@ type LambderGuardRefusalsKnown<TGuard, TRefusals> = TGuard extends {
386
426
  } ? NonNullable<TNamed> extends readonly (infer TCode extends string)[] ? string extends TCode ? unknown : [Exclude<TCode, keyof NonNullable<TRefusals> & string>] extends [never] ? unknown : {
387
427
  refusals: readonly (keyof NonNullable<TRefusals> & string)[];
388
428
  } : unknown : unknown;
429
+ /** Every policy in a map, or in each map of a list, held to the policy shape, its message included; a list also declares no name twice. */
430
+ type LambderRateLimitPolicyChecks<TPolicies> = TPolicies extends readonly unknown[] ? {
431
+ [TIndex in keyof TPolicies]: LambderRateLimitPolicyChecks<TPolicies[TIndex]>;
432
+ } & LambderNoRepeatedNames<TPolicies> : {
433
+ [TPolicy in keyof TPolicies]: LambderNoExtraKeys<TPolicies[TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext>> & (TPolicies[TPolicy] extends {
434
+ refusal: infer TMessage;
435
+ } ? {
436
+ refusal?: LambderNoExtraKeys<TMessage, LambderRateLimitMessage>;
437
+ } : unknown);
438
+ };
439
+ /** Every guard in a map, or in each map of a list, held to the guard shape and its refusals to the vocabulary; a list also declares no name twice. */
440
+ type LambderGuardChecks<TSessionData, TGuards, TRefusals> = TGuards extends readonly unknown[] ? {
441
+ [TIndex in keyof TGuards]: LambderGuardChecks<TSessionData, TGuards[TIndex], TRefusals>;
442
+ } & LambderNoRepeatedNames<TGuards> : {
443
+ [TGuard in keyof TGuards]: LambderNoExtraKeys<TGuards[TGuard], LambderGuardsMap<TSessionData>[string]> & LambderGuardRefusalsKnown<TGuards[TGuard], TRefusals>;
444
+ };
389
445
  /**
390
446
  * The surplus-key rule one level down, over the option objects a typo is
391
447
  * worst on. Excess-property checking is off for the WHOLE literal under
@@ -405,17 +461,9 @@ export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCrea
405
461
  idempotency?: LambderNoExtraKeys<NonNullable<TOptions["idempotency"]>, LambderOptionShape<TSessionData, "idempotency">>;
406
462
  crashes?: LambderNoExtraKeys<NonNullable<TOptions["crashes"]>, LambderOptionShape<TSessionData, "crashes">>;
407
463
  rateLimits?: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>, LambderOptionShape<TSessionData, "rateLimits">> & {
408
- policies?: {
409
- [TPolicy in keyof NonNullable<TOptions["rateLimits"]>["policies"]]: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>["policies"][TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext>> & (NonNullable<TOptions["rateLimits"]>["policies"][TPolicy] extends {
410
- refusal: infer TMessage;
411
- } ? {
412
- refusal?: LambderNoExtraKeys<TMessage, LambderRateLimitMessage>;
413
- } : unknown);
414
- };
415
- };
416
- guards?: {
417
- [TGuard in keyof NonNullable<TOptions["guards"]>]: LambderNoExtraKeys<NonNullable<TOptions["guards"]>[TGuard], LambderOptionShape<TSessionData, "guards">[string]> & LambderGuardRefusalsKnown<NonNullable<TOptions["guards"]>[TGuard], TRefusals>;
464
+ policies?: LambderRateLimitPolicyChecks<NonNullable<TOptions["rateLimits"]>["policies"]>;
418
465
  };
466
+ guards?: LambderGuardChecks<TSessionData, NonNullable<TOptions["guards"]>, TRefusals>;
419
467
  files?: TOptions["files"] extends {
420
468
  source: unknown;
421
469
  } ? LambderNoExtraKeys<TOptions["files"], Extract<LambderFilesOption, {
@@ -430,4 +478,4 @@ export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCrea
430
478
  * every API call (an apiPath with no leading slash) or a 500 on every
431
479
  * response (maxResponseBytes: 0) that an app discovers in production.
432
480
  */
433
- export declare const assertCreateOptions: (options: LambderCreateOptions<any>) => void;
481
+ export declare const assertCreateOptions: (options: LambderMergedCreateOptions<any>) => void;
@@ -1,5 +1,23 @@
1
1
  import { assertRefusalCodesDeclared, readRefusalVocabulary } from "../api/LambderApiRefusals.js";
2
2
  import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
3
+ /**
4
+ * A named-maps option as one map: a lone map as it is, a list merged in
5
+ * order. A name declared in two maps of the list is refused.
6
+ */
7
+ export const mergeNamedMaps = (option, what) => {
8
+ if (!Array.isArray(option))
9
+ return option;
10
+ const merged = {};
11
+ for (const map of option) {
12
+ for (const [name, declaration] of Object.entries(map)) {
13
+ if (Object.prototype.hasOwnProperty.call(merged, name)) {
14
+ throw new Error(`Lambder: the ${what} "${name}" is declared in two of the maps given to create(). Declare each name once.`);
15
+ }
16
+ merged[name] = declaration;
17
+ }
18
+ }
19
+ return merged;
20
+ };
3
21
  /**
4
22
  * Everything create() refuses before an instance exists, in one place: a
5
23
  * value that cannot work is a startup error naming the option, not a 404 on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "10.0.1",
3
+ "version": "10.1.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": [