lambder 15.0.2 → 16.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 (37) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +4 -4
  3. package/dist/api/LambderApiDeclarations.d.ts +21 -5
  4. package/dist/api/LambderApiDeclarations.js +21 -3
  5. package/dist/api/LambderApiGuards.d.ts +23 -3
  6. package/dist/api/LambderApiGuards.js +50 -25
  7. package/dist/api/LambderApiModules.d.ts +50 -0
  8. package/dist/api/LambderApiModules.js +36 -0
  9. package/dist/api/LambderApiPipeline.d.ts +27 -4
  10. package/dist/api/LambderApiPipeline.js +71 -5
  11. package/dist/api/LambderApiRateLimits.d.ts +35 -4
  12. package/dist/api/LambderApiRateLimits.js +74 -35
  13. package/dist/core/Lambder.d.ts +68 -93
  14. package/dist/core/Lambder.js +147 -71
  15. package/dist/core/LambderApiBuilders.d.ts +105 -0
  16. package/dist/core/LambderApiBuilders.js +12 -0
  17. package/dist/core/LambderApiModule.d.ts +106 -0
  18. package/dist/core/LambderApiModule.js +45 -0
  19. package/dist/core/LambderCallSummary.d.ts +11 -9
  20. package/dist/core/LambderCreateOptions.d.ts +29 -24
  21. package/dist/core/LambderInvocationScope.d.ts +9 -3
  22. package/dist/core/LambderRouting.d.ts +9 -0
  23. package/dist/core/LambderRouting.js +20 -0
  24. package/dist/index.d.ts +10 -5
  25. package/dist/index.js +2 -0
  26. package/dist/mock/LambderMockApiModule.d.ts +70 -0
  27. package/dist/mock/LambderMockApiModule.js +34 -0
  28. package/dist/mock/LambderMockApp.d.ts +53 -8
  29. package/dist/mock/LambderMockApp.js +104 -28
  30. package/dist/mock/LambderMockCreateOptions.d.ts +18 -18
  31. package/dist/mock/LambderMockTypes.d.ts +46 -26
  32. package/dist/mock.d.ts +4 -1
  33. package/dist/mock.js +2 -0
  34. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  35. package/dist/testing/LambderTestApp.d.ts +4 -3
  36. package/dist/testing/LambderTestApp.js +4 -3
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,124 @@ 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
+ ## [16.0.1] - 2026-11-08
13
+
14
+ A major. Modules: guards and rate-limit policies a part of an app declares
15
+ for its own endpoints, so the file creating an instance imports no part's;
16
+ the types an app's declaration files need, exported; and a mock's guards held
17
+ at the entry rather than at `create()`. Every break is named under Changed.
18
+
19
+ ### Added
20
+
21
+ - **`defineApiModule({ guards, rateLimitPolicies })`** on an instance, and
22
+ `LambderApiModule`, what it returns: `defineApi`, `defineApiGroup` and
23
+ `lazyApiGroup` typed on the instance's guards and policies with the
24
+ module's beside them, so a module's endpoints name either and an unknown
25
+ name is still a compile error, while the instance's own builders, and every
26
+ other module's, see none of the module's names. Registering a group adds
27
+ the modules its endpoints were declared with to the instance; a module's
28
+ `lazyApiGroup` carries the module, so its group adds it at registration,
29
+ before it loads, and `apiOptionEntries()`, `apiSignatures()`, the
30
+ generated tables and a route charging one of its policies all see it from
31
+ the start. A module reaches an instance only through its groups: on an
32
+ instance that registers none of them, a route charging one of its policies
33
+ fails at its first request. Each option takes one map or a list of maps,
34
+ held to what `create()` holds its own to. A name the instance declares is
35
+ refused where the module is defined, at compile time and at the call; a
36
+ name two modules declare is refused when the second is registered, and the
37
+ refused registration leaves the instance as it was. A module declaring
38
+ policies needs the instance's limiter. A lazy group registered with the
39
+ instance's own `lazyApiGroup` whose loaded endpoints a module declared is
40
+ refused when it loads. `LambderRenderContextOf<typeof module>` types a
41
+ helper handed one of its handlers' contexts.
42
+ - **`defineApiModule({ guards })` on a mock app**, and `LambderMockApiModule`:
43
+ `api`, `notMocked`, `apiSlice` and `override` with a module's mock guards
44
+ beside the app's, added when its entries are registered, each guard held to
45
+ the contract and the generated `guardDeclarations` as `create()`'s are, and
46
+ the same rule for names. A mock module takes no policies: the mock takes
47
+ them all at `create()`, from the generated table.
48
+ - **`rateLimits: { limiter }` with no policies**, for an instance whose
49
+ policies are all its modules'. An empty `policies: {}` is still refused.
50
+ - **The types an app's declaration files name are exported**, so an app that
51
+ emits declaration files (`declaration`, `composite`) no longer stops at
52
+ TS2883 ("cannot be named without a reference to ... from lambder"). The
53
+ builders are named types, `LambderDefineApi`, `LambderDefineApiGroup` and
54
+ `LambderDefineLazyApiGroup`, so a builder handed out destructured or under
55
+ the instance's name is declared by that name. Exported with them: the
56
+ fourteen names seen missing on an app's own declarations
57
+ (`LambderObjectOutputCheck`, `LambderPolicyPerOf`,
58
+ `LambderPublicSessionPolicyCheck`, `LambderSessionModeCheck`,
59
+ `LambderMaybePromise`, which was `MaybePromise` and unexported,
60
+ `LambderActionNamesCheck`, `LambderGroupNameCheck`,
61
+ `LambderGroupPartsCheck`, `LambderMergedParts`, `LambderGivenOption`,
62
+ `LambderInitCreateOptions`, `LambderMergedNamedMaps`,
63
+ `LambderNamedMapsOption`, `LambderNestedOptionChecks`);
64
+ `LambderRefusalVocabularyOptionChecks`, from both entries; the module types;
65
+ and the types written directly into a builder's, a method's or a
66
+ constructor's parameters (`LambderPayloadSliceCheck`,
67
+ `LambderRequirableGuardsField`, `LambderSessionEnabledInstance`,
68
+ `LambderRegisteredGroupsCheck`, `LambderNoExtraKeys`,
69
+ `LambderApiHandlerContext`, `LambderApiModuleOptions`,
70
+ `LambderApiModuleOptionChecks`, `LambderApiModuleDeclarations`, and on the
71
+ mock entry `LambderMockApiModuleOptions`, `LambderMockApiModuleChecks`,
72
+ `LambderMockApiModuleDoor`, `LambderMockEntryGuardsCheck` and
73
+ `LambderMockNotMockedGuardsCheck`). A test emits the declarations of a
74
+ sample app against the built package and fails on any diagnostic: a server
75
+ (an exported init, create() in a factory, the builders destructured and
76
+ under the instance's name, groups, lazy groups, a module, a list of parts,
77
+ the entry, routes, a test app, an invoke caller) and a mock (the vocabulary
78
+ declared, the generated tables given to create(), entries as handlers,
79
+ `notMocked`, a mock module, `register()`). The other names are kept for the
80
+ shapes the sample does not reach.
81
+
82
+ ### Changed
83
+
84
+ - **A mock's guards are held at the entry rather than at `create()`.** The
85
+ `guards` option need no longer name every guard the contract declares,
86
+ since a module may declare its own; each entry instead needs a mock guard
87
+ for every guard its endpoint declares in the map it is built with, and,
88
+ without the `apiOptions` table, a session endpoint's entry needs one
89
+ declared `session: true`. Both are compile errors on the entry's name. A
90
+ guard the map does name is still held to the contract.
91
+ `LambderMockSessionGuardsCheck` is removed, replaced by
92
+ `LambderMockEntryGuardsCheck` (and `LambderMockNotMockedGuardsCheck` for
93
+ `notMocked`). A `@ts-expect-error` on a `create()` that leaves out a guard
94
+ the contract declares is now unused (TS2578): the error is at the entry.
95
+ - **`api()` and `notMocked()` take their name as
96
+ `K & LambderMockEntryGuardsCheck<...>` and
97
+ `K & LambderMockNotMockedGuardsCheck<...>`.** A wrapper generic over the
98
+ endpoint name that passes its own type parameter on no longer compiles
99
+ (TS2345); call them with a concrete name, or cast the name in the wrapper.
100
+ - **`LambderApiRateLimitsConfig.policies` is optional**, matching the
101
+ `rateLimits` option above, so code reading `.policies` off one sees
102
+ `| undefined`.
103
+
104
+ ## [15.1.1] - 2026-10-03
105
+
106
+ A minor: every request a route answers writes a call summary line, named by
107
+ the route as registered, and a matcher object can name its route.
108
+
109
+ ### Added
110
+
111
+ - **A call summary line for every route.** A request a route answered
112
+ (`addRoute`, `addSessionRoute`, or an `addAction` on its HTTP side) writes
113
+ the same `lambder.call` line an API call does, so a webhook or a page route
114
+ is counted, timed and queried beside the endpoints. The line's `route`
115
+ names the route as it was registered, its matcher's `name` or its method
116
+ and path pattern, and carries nothing of the request's own path; its
117
+ `outcome` is read from its status (`success` below 400, `crash` from 500,
118
+ `other` between) and `handlerMs` is the route handler's own time. A line
119
+ a hook's answer ends in is the matched route's too. Files, the index page,
120
+ the route fallback and non-HTTP events still write none.
121
+ - **`name` on a matcher object**, what the line names the route by, which a
122
+ bare predicate route needs to be named at all.
123
+
124
+ ### Changed
125
+
126
+ - **`LambderCallSummary.api` is null for a route, and the line gains
127
+ `route`**, null for an API call. A `callSummary` function that read `api`
128
+ as a string narrows it first.
129
+
12
130
  ## [15.0.2] - 2026-10-02
13
131
 
14
132
  A patch: two fixes for apps with a large contract.
package/README.md CHANGED
@@ -80,10 +80,10 @@ const company = await caller.companies.get({ slug: "acme" });
80
80
  cookies, and a guard against Lambda's response size cap.
81
81
  - **Hooks and actions.** Lifecycle hooks, plus `addAction()` for the non-HTTP
82
82
  invocations (EventBridge, SQS, custom events) the same function receives.
83
- - **A summary line per call.** Every API call is logged once, as JSON, with
84
- its endpoint, outcome, refusal code, status and timings, and the request
85
- ids that join it to the calls it made; nothing from its input or its
86
- caller.
83
+ - **A summary line per call.** Every API call, and every request a route
84
+ answers, is logged once, as JSON, with its endpoint or route, outcome,
85
+ refusal code, status and timings, and the request ids that join it to the
86
+ calls it made; nothing from its input, its path or its caller.
87
87
  - **Lambda to lambda calls.** `LambderInvokeCaller` invokes a Lambder app in
88
88
  another function directly, with no API Gateway in between, typed from the
89
89
  callee's own contract and carrying its refusals, crash detail and logs back.
@@ -1,9 +1,10 @@
1
1
  import type { z } from "zod";
2
- import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
2
+ import type { LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
3
3
  import type { LambderApiMode, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
4
4
  import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue, LambderRefusalsOptionValue } from "../shared/wire/LambderApiOptionValues.js";
5
5
  import type { LambderApiRateLimitPolicyConfig, LambderPolicyPerOf } from "./LambderApiRateLimits.js";
6
6
  import type { LambderRefusalVocabulary } from "./LambderApiRefusals.js";
7
+ import type { LambderApiModuleDeclarations, LambderApiModuleUse } from "./LambderApiModules.js";
7
8
  import { type LambderReservedActionName, type LambderReservedGroupName } from "../shared/wire/LambderApiNames.js";
8
9
  /**
9
10
  * What an instance's declarations are typed against, fixed at create(): the
@@ -83,12 +84,14 @@ declare const LAMBDER_CONTRACT_ENTRY: unique symbol;
83
84
  /**
84
85
  * One endpoint, declared: its options and its handler, and in its type the
85
86
  * contract entry a client calls it by. Built by an instance's defineApi(),
86
- * registered by being put in a group.
87
+ * or a module's, registered by being put in a group.
87
88
  */
88
89
  export type LambderApiDeclaration<TEntry = unknown> = {
89
90
  readonly kind: "lambderApi";
90
91
  readonly options: LambderApiDeclarationOptions;
91
- readonly handler: (ctx: never) => MaybePromise<unknown>;
92
+ readonly handler: (ctx: never) => LambderMaybePromise<unknown>;
93
+ /** The module whose builders declared it (defineApiModule), whose guards and policies its group's registration adds; absent on the instance's own. */
94
+ readonly module?: LambderApiModuleDeclarations;
92
95
  readonly [LAMBDER_CONTRACT_ENTRY]: TEntry;
93
96
  };
94
97
  /** A group's endpoints by action name. */
@@ -108,6 +111,12 @@ export type LambderLazyApiGroup<TName extends string = string, TGroup extends La
108
111
  readonly kind: "lambderLazyApiGroup";
109
112
  readonly name: TName;
110
113
  readonly load: () => Promise<TGroup>;
114
+ /**
115
+ * The module whose lazyApiGroup registered it, whose guards and policies
116
+ * its registration adds at once, before the group loads; absent on the
117
+ * instance's own.
118
+ */
119
+ readonly module?: LambderApiModuleDeclarations;
111
120
  };
112
121
  /** Either kind of group registerApiGroups() takes. */
113
122
  export type LambderRegistrableApiGroup = LambderApiGroup<string, any> | LambderLazyApiGroup<string, any>;
@@ -193,8 +202,15 @@ export type LambderActionNamesCheck<TApis> = [
193
202
  * usually one part; one declared across files gives each file's part.
194
203
  */
195
204
  export declare const buildApiGroup: <TName extends string, TApis extends LambderApiDeclarations>(name: TName, parts: readonly LambderApiDeclarations[]) => LambderApiGroup<TName, TApis>;
196
- /** Builds a lazy group; the name is checked here, the group it loads when it loads. */
197
- export declare const buildLazyApiGroup: <TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, TGroup>;
205
+ /** Builds a lazy group, carrying the module that registers it, if one does; the name is checked here, the group it loads when it loads. */
206
+ export declare const buildLazyApiGroup: <TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName, load: () => Promise<TGroup>, module: LambderApiModuleDeclarations | null) => LambderLazyApiGroup<TName, TGroup>;
207
+ /**
208
+ * The modules a group brings to its registration, each with where it was
209
+ * met: a lazy group's own (the module whose lazyApiGroup registered it), and
210
+ * a group's endpoints' (the modules whose builders declared them), each
211
+ * module once.
212
+ */
213
+ export declare const apiModuleUsesOf: (group: LambderRegistrableApiGroup) => LambderApiModuleUse[];
198
214
  /** Whether a value is either kind of group registerApiGroups() takes. */
199
215
  export declare const isRegistrableApiGroup: (value: unknown) => value is LambderRegistrableApiGroup;
200
216
  export {};
@@ -36,14 +36,32 @@ export const buildApiGroup = (name, parts) => {
36
36
  }
37
37
  return { kind: "lambderApiGroup", name, apis: apis };
38
38
  };
39
- /** Builds a lazy group; the name is checked here, the group it loads when it loads. */
40
- export const buildLazyApiGroup = (name, load) => {
39
+ /** Builds a lazy group, carrying the module that registers it, if one does; the name is checked here, the group it loads when it loads. */
40
+ export const buildLazyApiGroup = (name, load, module) => {
41
41
  if (typeof name !== "string" || !isGroupName(name)) {
42
42
  throw new Error(`Lambder: ${describeName(name)} cannot name a group. A group is a letter then letters, digits or underscores, and not a name a caller already answers for.`);
43
43
  }
44
44
  if (typeof load !== "function")
45
45
  throw new Error(`Lambder: lazy group "${name}" has no loader. Pass a function that imports the group: () => import("./orders.js").then((m) => m.orderApis).`);
46
- return { kind: "lambderLazyApiGroup", name, load };
46
+ return module ? { kind: "lambderLazyApiGroup", name, load, module } : { kind: "lambderLazyApiGroup", name, load };
47
+ };
48
+ /**
49
+ * The modules a group brings to its registration, each with where it was
50
+ * met: a lazy group's own (the module whose lazyApiGroup registered it), and
51
+ * a group's endpoints' (the modules whose builders declared them), each
52
+ * module once.
53
+ */
54
+ export const apiModuleUsesOf = (group) => {
55
+ if (group.kind === "lambderLazyApiGroup") {
56
+ return group.module ? [{ module: group.module, usedBy: `registering lazy group "${group.name}"` }] : [];
57
+ }
58
+ const uses = [];
59
+ for (const [action, declaration] of Object.entries(group.apis)) {
60
+ const module = declaration.module;
61
+ if (module && !uses.some((use) => use.module === module))
62
+ uses.push({ module, usedBy: `declaring "${group.name}.${action}"` });
63
+ }
64
+ return uses;
47
65
  };
48
66
  /** Whether a value is either kind of group registerApiGroups() takes. */
49
67
  export const isRegistrableApiGroup = (value) => {
@@ -390,6 +390,14 @@ export declare const toGuardEntries: (value?: LambderGuardsOptionValue) => {
390
390
  name: string;
391
391
  param: unknown;
392
392
  }[];
393
+ /**
394
+ * Refuses a guard that cannot run: no handler, both input modes, a refusals
395
+ * option that is not a list of codes, an unknown runAt, a singleUseInput
396
+ * that is not a boolean or sits on a guard without a guardInput. What the
397
+ * guards option at creation and a module's guards (defineApiModule) are each
398
+ * held to, one rule for both.
399
+ */
400
+ export declare const assertGuardDeclaration: (name: string, guardDef: LambderApiGuard<any, any, any>) => void;
393
401
  /**
394
402
  * Runtime side of the guards subsystem: holds the defined guards, asserts
395
403
  * API registrations against them at startup, and executes an API's declared
@@ -399,12 +407,24 @@ export declare const toGuardEntries: (value?: LambderGuardsOptionValue) => {
399
407
  */
400
408
  export declare class LambderApiGuardsEngine {
401
409
  private guards;
402
- /** True once a guards map was configured. */
410
+ /** True once a guards map was configured, or a module added guards. */
403
411
  get isConfigured(): boolean;
412
+ /** Whether a guard of this name is declared, by the creation's map or a module added since. */
413
+ has(name: string): boolean;
404
414
  /** Take the guards map given at creation; named like the other two engines' configure(). */
405
415
  configure(guards: Record<string, LambderApiGuard<any, any, any>>): void;
406
- /** Startup validation of one API registration's guards option. */
407
- assertRegistration(apiName: string, mode: LambderApiMode, guardsOption?: LambderGuardsOptionValue): void;
416
+ /**
417
+ * Adds a module's guards beside the ones declared so far. Its names were
418
+ * checked against them before (LambderApiPipeline.modulesToAdd), so a
419
+ * name met again here is a caller that skipped that check.
420
+ */
421
+ add(guards: Readonly<Record<string, LambderApiGuard<any, any, any>>>): void;
422
+ /**
423
+ * Startup validation of one API registration's guards option. `adding`
424
+ * holds the guards of the modules the same registration adds, which the
425
+ * engine takes only once every endpoint of it passed.
426
+ */
427
+ assertRegistration(apiName: string, mode: LambderApiMode, guardsOption?: LambderGuardsOptionValue, adding?: Readonly<Record<string, LambderApiGuard<any, any, any>>>): void;
408
428
  /**
409
429
  * The posted guard inputs an idempotency fingerprint holds for an API
410
430
  * declaring `guardsOption`: the value of each of its `guardInput` guards
@@ -35,6 +35,34 @@ export const toGuardEntries = (value) => {
35
35
  * unrestricted, so the read has to handle this.
36
36
  */
37
37
  const readGuardInput = (guardInputs, name) => guardInputs !== undefined && Object.prototype.hasOwnProperty.call(guardInputs, name) ? guardInputs[name] : undefined;
38
+ /**
39
+ * Refuses a guard that cannot run: no handler, both input modes, a refusals
40
+ * option that is not a list of codes, an unknown runAt, a singleUseInput
41
+ * that is not a boolean or sits on a guard without a guardInput. What the
42
+ * guards option at creation and a module's guards (defineApiModule) are each
43
+ * held to, one rule for both.
44
+ */
45
+ export const assertGuardDeclaration = (name, guardDef) => {
46
+ if (typeof guardDef?.handler !== "function")
47
+ throw new Error(`Lambder: guard "${name}" has no handler function.`);
48
+ if (guardDef.apiInput && guardDef.guardInput)
49
+ throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
50
+ const refusals = guardDef.refusals;
51
+ if (refusals !== undefined && (!Array.isArray(refusals) || refusals.some((code) => typeof code !== "string" || code === ""))) {
52
+ throw new Error(`Lambder: guard "${name}" has a refusals option that is not a list of refusal codes.`);
53
+ }
54
+ const runAt = guardDef.runAt;
55
+ if (runAt !== undefined && runAt !== "beforeInputValidation" && runAt !== "afterInputValidation") {
56
+ throw new Error(`Lambder: guard "${name}" has runAt "${String(runAt)}"; use "beforeInputValidation" (default) or "afterInputValidation".`);
57
+ }
58
+ const singleUseInput = guardDef.singleUseInput;
59
+ if (singleUseInput !== undefined && typeof singleUseInput !== "boolean") {
60
+ throw new Error(`Lambder: guard "${name}" has singleUseInput ${String(singleUseInput)}; write true for a value a retry carries fresh, or leave it off.`);
61
+ }
62
+ if (singleUseInput && !guardDef.guardInput) {
63
+ throw new Error(`Lambder: guard "${name}" declares singleUseInput without a guardInput. Only a value the client sends beside the payload can be left out of an idempotent request's fingerprint; a slice of the payload always counts.`);
64
+ }
65
+ };
38
66
  /**
39
67
  * Runtime side of the guards subsystem: holds the defined guards, asserts
40
68
  * API registrations against them at startup, and executes an API's declared
@@ -49,8 +77,10 @@ export class LambderApiGuardsEngine {
49
77
  // that typo and then fail on every request. It would also refuse to
50
78
  // register a guard legitimately named one of them.
51
79
  guards = new Map();
52
- /** True once a guards map was configured. */
80
+ /** True once a guards map was configured, or a module added guards. */
53
81
  get isConfigured() { return this.guards.size > 0; }
82
+ /** Whether a guard of this name is declared, by the creation's map or a module added since. */
83
+ has(name) { return this.guards.has(name); }
54
84
  /** Take the guards map given at creation; named like the other two engines' configure(). */
55
85
  configure(guards) {
56
86
  // One configuration per instance, as in the rate-limit and idempotency
@@ -65,33 +95,28 @@ export class LambderApiGuardsEngine {
65
95
  if (Object.keys(guards).length === 0) {
66
96
  throw new Error("Lambder: the guards option was declared with no guards in it, which configures nothing. Name the guards APIs will declare, or leave the option off.");
67
97
  }
98
+ this.add(guards);
99
+ }
100
+ /**
101
+ * Adds a module's guards beside the ones declared so far. Its names were
102
+ * checked against them before (LambderApiPipeline.modulesToAdd), so a
103
+ * name met again here is a caller that skipped that check.
104
+ */
105
+ add(guards) {
68
106
  for (const [name, guardDef] of Object.entries(guards)) {
69
107
  if (this.guards.has(name))
70
108
  throw new Error(`Lambder: guard "${name}" is already defined.`);
71
- if (typeof guardDef?.handler !== "function")
72
- throw new Error(`Lambder: guard "${name}" has no handler function.`);
73
- if (guardDef.apiInput && guardDef.guardInput)
74
- throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
75
- const refusals = guardDef.refusals;
76
- if (refusals !== undefined && (!Array.isArray(refusals) || refusals.some((code) => typeof code !== "string" || code === ""))) {
77
- throw new Error(`Lambder: guard "${name}" has a refusals option that is not a list of refusal codes.`);
78
- }
79
- const runAt = guardDef.runAt;
80
- if (runAt !== undefined && runAt !== "beforeInputValidation" && runAt !== "afterInputValidation") {
81
- throw new Error(`Lambder: guard "${name}" has runAt "${String(runAt)}"; use "beforeInputValidation" (default) or "afterInputValidation".`);
82
- }
83
- const singleUseInput = guardDef.singleUseInput;
84
- if (singleUseInput !== undefined && typeof singleUseInput !== "boolean") {
85
- throw new Error(`Lambder: guard "${name}" has singleUseInput ${String(singleUseInput)}; write true for a value a retry carries fresh, or leave it off.`);
86
- }
87
- if (singleUseInput && !guardDef.guardInput) {
88
- throw new Error(`Lambder: guard "${name}" declares singleUseInput without a guardInput. Only a value the client sends beside the payload can be left out of an idempotent request's fingerprint; a slice of the payload always counts.`);
89
- }
90
- this.guards.set(name, guardDef);
109
+ assertGuardDeclaration(name, guardDef);
91
110
  }
111
+ for (const [name, guardDef] of Object.entries(guards))
112
+ this.guards.set(name, guardDef);
92
113
  }
93
- /** Startup validation of one API registration's guards option. */
94
- assertRegistration(apiName, mode, guardsOption) {
114
+ /**
115
+ * Startup validation of one API registration's guards option. `adding`
116
+ * holds the guards of the modules the same registration adds, which the
117
+ * engine takes only once every endpoint of it passed.
118
+ */
119
+ assertRegistration(apiName, mode, guardsOption, adding) {
95
120
  const entries = toGuardEntries(guardsOption);
96
121
  // The runtime half of LambderNonEmptyOptionMap on the guards option.
97
122
  // `guards: {}` and `guards: []` would satisfy the require*ApiGuards
@@ -103,9 +128,9 @@ export class LambderApiGuardsEngine {
103
128
  `Name the guard that authorizes it, or omit the option entirely.`);
104
129
  }
105
130
  for (const { name } of entries) {
106
- const guardDef = this.guards.get(name);
131
+ const guardDef = this.guards.get(name) ?? (adding && Object.prototype.hasOwnProperty.call(adding, name) ? adding[name] : undefined);
107
132
  if (!guardDef) {
108
- throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation.`);
133
+ throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation, or in the module the API is declared with (defineApiModule).`);
109
134
  }
110
135
  if (guardDef.session && mode !== "session") {
111
136
  throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true) but is registered as public. An endpoint declaring a session guard is a session endpoint.`);
@@ -0,0 +1,50 @@
1
+ import { type LambderApiGuard } from "./LambderApiGuards.js";
2
+ import { type LambderApiRateLimitPolicyConfig } from "./LambderApiRateLimits.js";
3
+ import { type LambderNamedMapsOption } from "../shared/util/LambderNamedMaps.js";
4
+ /** One map of guards, of either adapter's contexts. */
5
+ type LambderModuleGuards = Readonly<Record<string, LambderApiGuard<any, any, any, any, any>>>;
6
+ /** One map of rate-limit policies, of either adapter's contexts. */
7
+ type LambderModulePolicies = Readonly<Record<string, LambderApiRateLimitPolicyConfig<any>>>;
8
+ /**
9
+ * What a module declares, as registration adds it: its guards and its
10
+ * rate-limit policies, merged into one map each. One object per module, made
11
+ * once by defineApiModule() and carried by every endpoint its builders
12
+ * declare and every lazy group its lazyApiGroup registers: the object is how
13
+ * a registration finds a module and tells two apart.
14
+ */
15
+ export type LambderApiModuleDeclarations = {
16
+ readonly guards: LambderModuleGuards;
17
+ readonly rateLimitPolicies: LambderModulePolicies;
18
+ };
19
+ /** A module a registration brings, and where it was met, for the error naming a name two declare. */
20
+ export type LambderApiModuleUse = {
21
+ readonly module: LambderApiModuleDeclarations;
22
+ /** How the error names the module: "declaring group \"orders\"", say. */
23
+ readonly usedBy: string;
24
+ };
25
+ /**
26
+ * The modules one registration adds, checked against what the instance
27
+ * declares and against each other, their declarations merged. Registration
28
+ * checks every endpoint against these and the instance's own before the
29
+ * pipeline takes them (LambderApiPipeline.addModules), so a refused
30
+ * registration leaves the instance as it was.
31
+ */
32
+ export type LambderAddedApiModules = {
33
+ readonly modules: readonly LambderApiModuleDeclarations[];
34
+ readonly guards: LambderModuleGuards;
35
+ readonly rateLimitPolicies: LambderModulePolicies;
36
+ };
37
+ /** What a registration checks an endpoint's names against beside the instance's own: the declarations of the modules it is adding, or of the one module building an entry. */
38
+ export type LambderPendingDeclarations = Pick<LambderAddedApiModules, "guards" | "rateLimitPolicies">;
39
+ /**
40
+ * A module's declarations from what defineApiModule() was given: each option
41
+ * one map or a list of them, merged, a name two of a list's maps declare
42
+ * refused; every guard and policy held to the rules the creation's are held
43
+ * to. A module declaring nothing is refused, as an empty guards option is:
44
+ * its builders would be the instance's own.
45
+ */
46
+ export declare const apiModuleDeclarationsOf: (options: {
47
+ guards?: LambderNamedMapsOption<LambderModuleGuards>;
48
+ rateLimitPolicies?: LambderNamedMapsOption<LambderModulePolicies>;
49
+ }, owner: "Lambder" | "LambderMockApp") => LambderApiModuleDeclarations;
50
+ export {};
@@ -0,0 +1,36 @@
1
+ import { assertGuardDeclaration } from "./LambderApiGuards.js";
2
+ import { assertRateLimitPolicyDeclaration } from "./LambderApiRateLimits.js";
3
+ import { mergeNamedMaps } from "../shared/util/LambderNamedMaps.js";
4
+ /** A record with no prototype, so a declaration named "__proto__" or "constructor" is a key like any other. */
5
+ const bareRecord = () => Object.create(null);
6
+ /**
7
+ * A module's declarations from what defineApiModule() was given: each option
8
+ * one map or a list of them, merged, a name two of a list's maps declare
9
+ * refused; every guard and policy held to the rules the creation's are held
10
+ * to. A module declaring nothing is refused, as an empty guards option is:
11
+ * its builders would be the instance's own.
12
+ */
13
+ export const apiModuleDeclarationsOf = (options, owner) => {
14
+ const guards = bareRecord();
15
+ const rateLimitPolicies = bareRecord();
16
+ if (options.guards !== undefined) {
17
+ for (const [name, guard] of Object.entries(mergeNamedMaps(options.guards, "guard", "defineApiModule()"))) {
18
+ assertGuardDeclaration(name, guard);
19
+ guards[name] = guard;
20
+ }
21
+ }
22
+ if (options.rateLimitPolicies !== undefined) {
23
+ for (const [name, policy] of Object.entries(mergeNamedMaps(options.rateLimitPolicies, "rate-limit policy", "defineApiModule()"))) {
24
+ assertRateLimitPolicyDeclaration(name, policy);
25
+ rateLimitPolicies[name] = policy;
26
+ }
27
+ }
28
+ if (Object.keys(guards).length === 0 && Object.keys(rateLimitPolicies).length === 0) {
29
+ throw new Error(owner === "Lambder"
30
+ ? "Lambder: defineApiModule() was given no guard and no rate-limit policy, which declares nothing a module's endpoints could name " +
31
+ "beyond the instance's own. Declare the module's guards or rate-limit policies, or declare its endpoints with the instance's builders."
32
+ : "LambderMockApp: defineApiModule() was given no guard, which declares nothing a module's entries could name beyond the app's own. " +
33
+ "Declare the module's mock guards, or build its entries with the app's own builders.");
34
+ }
35
+ return Object.freeze({ guards: Object.freeze(guards), rateLimitPolicies: Object.freeze(rateLimitPolicies) });
36
+ };
@@ -10,13 +10,14 @@ import { type LambderEndpointRefusals } from "./LambderApiRefusals.js";
10
10
  import { type LambderApiGuard } from "./LambderApiGuards.js";
11
11
  import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig, type LambderRateLimitChargeResult, type LambderRateLimitChargeSubject } from "./LambderApiRateLimits.js";
12
12
  import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
13
+ import type { LambderAddedApiModules, LambderApiModuleDeclarations, LambderApiModuleUse, LambderPendingDeclarations } from "./LambderApiModules.js";
13
14
  import type { LambderSessionRecord, LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
14
15
  import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
15
16
  import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
16
17
  import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
17
18
  import type LambderSessionManager from "../session/LambderSessionManager.js";
18
19
  import LambderSessionController, { type LambderSessionCookieOptions, type LambderSessionRequestInfo } from "../session/LambderSessionController.js";
19
- import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
20
+ import type { LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
20
21
  /**
21
22
  * The app's own answer for a rejected input (setApiInputValidationErrorHandler
22
23
  * on the server). Returning null asks for the standard 422 body, which only
@@ -24,7 +25,7 @@ import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
24
25
  * API's schema and every preflight slice (guard inputs, rate-limit keys)
25
26
  * answer through here, so one failure has one shape.
26
27
  */
27
- export type LambderApiInputRefusal<TCtx> = (zodError: z.ZodError, ctx: TCtx, request: LambderApiRequest) => MaybePromise<LambderApiAnswer | null>;
28
+ export type LambderApiInputRefusal<TCtx> = (zodError: z.ZodError, ctx: TCtx, request: LambderApiRequest) => LambderMaybePromise<LambderApiAnswer | null>;
28
29
  /** The session subsystem as the pipeline runs it: the manager plus the cookie names and scope the controller writes. */
29
30
  export type LambderApiSessionsConfig<TSessionData> = {
30
31
  manager: LambderSessionManager<TSessionData>;
@@ -140,6 +141,8 @@ export declare class LambderApiPipeline<TCtx extends LambderApiCallContext<TSess
140
141
  private readonly onInvalidInput;
141
142
  private readonly sessions;
142
143
  private readonly apiSignatures;
144
+ /** The modules whose guards and rate-limit policies this pipeline took (addModules), each once. */
145
+ private readonly modules;
143
146
  constructor(options?: LambderApiPipelineOptions<TCtx, TSessionData>);
144
147
  /** True when a session manager was configured. */
145
148
  get hasSessions(): boolean;
@@ -165,8 +168,28 @@ export declare class LambderApiPipeline<TCtx extends LambderApiCallContext<TSess
165
168
  * being counted, since only it knows whether the request is an API call.
166
169
  */
167
170
  chargeRateLimit(name: string, subject: LambderRateLimitChargeSubject): Promise<LambderRateLimitChargeResult>;
168
- /** Registration-time checks of one definition's declarative options; the same messages on the server and in the mock. */
169
- assertRegistration(definition: LambderApiDefinition): void;
171
+ /** Whether this pipeline took the module's guards and rate-limit policies already. */
172
+ hasModule(module: LambderApiModuleDeclarations): boolean;
173
+ /**
174
+ * The modules among `uses` this pipeline has not taken yet, checked and
175
+ * merged, and nothing taken: a name one of them declares that this
176
+ * pipeline declares already (at creation, or by a module taken before)
177
+ * or that another of them declares is refused, as create() refuses a name
178
+ * two maps declare, and so are rate-limit policies on a pipeline with no
179
+ * limiter for them. A module met twice is one module. addModules() takes
180
+ * what this returns once every endpoint of the registration passed.
181
+ */
182
+ modulesToAdd(uses: readonly LambderApiModuleUse[]): LambderAddedApiModules;
183
+ /** Takes the guards and rate-limit policies of the modules modulesToAdd() checked: from here on its endpoints run them, and code may charge the policies. */
184
+ addModules(added: LambderAddedApiModules): void;
185
+ /**
186
+ * Registration-time checks of one definition's declarative options; the
187
+ * same messages on the server and in the mock. `adding` holds the guards
188
+ * and policies a registration is about to add beside this pipeline's own:
189
+ * those of the modules it brings (modulesToAdd), or of the module an
190
+ * entry is built with.
191
+ */
192
+ assertRegistration(definition: LambderApiDefinition, adding?: LambderPendingDeclarations): void;
170
193
  /**
171
194
  * The answer for a request naming no registered API: the apiNotFound
172
195
  * refusal, carrying whatever the call already wrote (a CORS header, a