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.
- package/CHANGELOG.md +118 -0
- package/README.md +4 -4
- package/dist/api/LambderApiDeclarations.d.ts +21 -5
- package/dist/api/LambderApiDeclarations.js +21 -3
- package/dist/api/LambderApiGuards.d.ts +23 -3
- package/dist/api/LambderApiGuards.js +50 -25
- package/dist/api/LambderApiModules.d.ts +50 -0
- package/dist/api/LambderApiModules.js +36 -0
- package/dist/api/LambderApiPipeline.d.ts +27 -4
- package/dist/api/LambderApiPipeline.js +71 -5
- package/dist/api/LambderApiRateLimits.d.ts +35 -4
- package/dist/api/LambderApiRateLimits.js +74 -35
- package/dist/core/Lambder.d.ts +68 -93
- package/dist/core/Lambder.js +147 -71
- package/dist/core/LambderApiBuilders.d.ts +105 -0
- package/dist/core/LambderApiBuilders.js +12 -0
- package/dist/core/LambderApiModule.d.ts +106 -0
- package/dist/core/LambderApiModule.js +45 -0
- package/dist/core/LambderCallSummary.d.ts +11 -9
- package/dist/core/LambderCreateOptions.d.ts +29 -24
- package/dist/core/LambderInvocationScope.d.ts +9 -3
- package/dist/core/LambderRouting.d.ts +9 -0
- package/dist/core/LambderRouting.js +20 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2 -0
- package/dist/mock/LambderMockApiModule.d.ts +70 -0
- package/dist/mock/LambderMockApiModule.js +34 -0
- package/dist/mock/LambderMockApp.d.ts +53 -8
- package/dist/mock/LambderMockApp.js +104 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +18 -18
- package/dist/mock/LambderMockTypes.d.ts +46 -26
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +2 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/testing/LambderTestApp.d.ts +4 -3
- package/dist/testing/LambderTestApp.js +4 -3
- 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
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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) =>
|
|
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
|
|
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
|
-
/**
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
94
|
-
|
|
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 {
|
|
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) =>
|
|
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
|
-
/**
|
|
169
|
-
|
|
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
|