lambder 15.1.1 → 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.
@@ -118,8 +118,12 @@ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy
118
118
  export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
119
119
  /** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
120
120
  limiter: LambderRateLimiter;
121
- /** Named policies referenced (typed) from defineApi. */
122
- policies: TPolicies;
121
+ /**
122
+ * Named policies referenced (typed) from defineApi. Left out where every
123
+ * policy is declared by the app's modules (defineApiModule), which count
124
+ * on this limiter; an empty map is refused.
125
+ */
126
+ policies?: TPolicies;
123
127
  /**
124
128
  * Let the request through when the limiter itself fails (the table is
125
129
  * down, an IAM action is missing), instead of failing the request.
@@ -270,6 +274,14 @@ export type LambderRateLimitOption<TPolicies> = LambderAllowedPolicyNames<TPolic
270
274
  * session-keyed limits, before the guards, for "beforeGuards".
271
275
  */
272
276
  type LambderRateLimitPhase = "beforeSession" | LambderRateLimitChargeAt;
277
+ /**
278
+ * Refuses a policy that cannot be counted: a per that is no key, no window,
279
+ * a window no limiter can act on, a chargeAt on a policy that takes none, a
280
+ * message carrying a code or data, an unknown budget. What the policies
281
+ * given at creation and a module's policies (defineApiModule) are each held
282
+ * to, one rule for both.
283
+ */
284
+ export declare const assertRateLimitPolicyDeclaration: (name: string, policy: LambderApiRateLimitPolicyConfig) => void;
273
285
  /**
274
286
  * Runtime side of the rate-limit subsystem: holds the limiter and its named
275
287
  * policies, asserts API registrations against them at startup, and checks an
@@ -299,15 +311,34 @@ export declare class LambderApiRateLimitsEngine {
299
311
  constructor(keyFieldDigest: LambderKeyFieldDigest);
300
312
  /** True once rateLimits were configured. */
301
313
  get isConfigured(): boolean;
314
+ /** Whether a policy of this name is declared, by the creation's policies or a module added since. */
315
+ has(name: string): boolean;
316
+ /**
317
+ * Takes the limiter and the policies given at creation. The policies may
318
+ * be left out when every policy is a module's (defineApiModule), which
319
+ * counts on this limiter; an empty map is refused, as an empty guards
320
+ * map is.
321
+ */
302
322
  configure(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
323
+ /**
324
+ * Adds a module's policies beside the ones declared so far, counted on
325
+ * the limiter given at creation. Its names were checked against them
326
+ * before (LambderApiPipeline.modulesToAdd), so a name met again here is a
327
+ * caller that skipped that check.
328
+ */
329
+ add(policies: Readonly<Record<string, LambderApiRateLimitPolicyConfig>>): void;
303
330
  /**
304
331
  * Puts the engine over another limiter, for `lambder/testing`; the named
305
332
  * policies and failOpen stay as configured. False when rateLimits were
306
333
  * never configured: there is nothing for a limiter to sit under.
307
334
  */
308
335
  [LAMBDER_BACKEND_SWAP](limiter: LambderRateLimiter): boolean;
309
- /** Startup validation of one API registration's rateLimit option. */
310
- assertRegistration(apiName: string, mode: LambderApiMode, rateLimitOption?: LambderRateLimitOptionValue): void;
336
+ /**
337
+ * Startup validation of one API registration's rateLimit option.
338
+ * `adding` holds the policies of the modules the same registration adds,
339
+ * which the engine takes only once every endpoint of it passed.
340
+ */
341
+ assertRegistration(apiName: string, mode: LambderApiMode, rateLimitOption?: LambderRateLimitOptionValue, adding?: Readonly<Record<string, LambderApiRateLimitPolicyConfig>>): void;
311
342
  /**
312
343
  * Check the API's policies in declared order; the first exceeded one
313
344
  * refuses with a 429 envelope and a Retry-After header. Attempts count,
@@ -123,6 +123,37 @@ const windowsOf = (policy, override) => {
123
123
  const trackerKeyOf = (name, policy, apiName, key) => policy.budget === "perPolicy" || apiName === null
124
124
  ? joinKeyFields("policy", name, key)
125
125
  : joinKeyFields("api", apiName, name, key);
126
+ /**
127
+ * Refuses a policy that cannot be counted: a per that is no key, no window,
128
+ * a window no limiter can act on, a chargeAt on a policy that takes none, a
129
+ * message carrying a code or data, an unknown budget. What the policies
130
+ * given at creation and a module's policies (defineApiModule) are each held
131
+ * to, one rule for both.
132
+ */
133
+ export const assertRateLimitPolicyDeclaration = (name, policy) => {
134
+ const per = policy?.per;
135
+ if (per !== undefined && per !== "ip" && per !== "session" && typeof per?.handler !== "function") {
136
+ throw new Error(`Lambder: rate-limit policy "${name}" has a per that is not "ip", "session", or a { apiInput?, handler } key. Leave per out for a policy the handler charges with its own key.`);
137
+ }
138
+ if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
139
+ throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
140
+ }
141
+ assertWindowLimits(`rate-limit policy "${name}"`, policy);
142
+ const chargeAt = policy.chargeAt;
143
+ if (chargeAt !== undefined) {
144
+ if (chargeAt !== "beforeGuards" && chargeAt !== "afterGuards") {
145
+ throw new Error(`Lambder: rate-limit policy "${name}" has chargeAt "${String(chargeAt)}"; use "afterGuards" (default) or "beforeGuards".`);
146
+ }
147
+ if (per === undefined || per === "ip" || per === "session") {
148
+ throw new Error(`Lambder: rate-limit policy "${name}" sets chargeAt, which only a policy keyed by a { apiInput?, handler } key takes: per "ip" and per "session" each run at one fixed place, and a policy without per is charged by the code that names it.`);
149
+ }
150
+ }
151
+ assertRateLimitMessage(`rate-limit policy "${name}"`, policy.refusal);
152
+ const budget = policy.budget;
153
+ if (budget !== undefined && budget !== "perApi" && budget !== "perPolicy") {
154
+ throw new Error(`Lambder: rate-limit policy "${name}" has budget "${String(budget)}"; use "perApi" (default: each referencing API counts separately) or "perPolicy" (one counter shared by every referencing API).`);
155
+ }
156
+ };
126
157
  /**
127
158
  * Runtime side of the rate-limit subsystem: holds the limiter and its named
128
159
  * policies, asserts API registrations against them at startup, and checks an
@@ -157,40 +188,27 @@ export class LambderApiRateLimitsEngine {
157
188
  }
158
189
  /** True once rateLimits were configured. */
159
190
  get isConfigured() { return this.limiter !== null; }
191
+ /** Whether a policy of this name is declared, by the creation's policies or a module added since. */
192
+ has(name) { return this.policies.has(name); }
193
+ /**
194
+ * Takes the limiter and the policies given at creation. The policies may
195
+ * be left out when every policy is a module's (defineApiModule), which
196
+ * counts on this limiter; an empty map is refused, as an empty guards
197
+ * map is.
198
+ */
160
199
  configure(config) {
161
200
  if (this.limiter)
162
201
  throw new Error("Lambder: rateLimits were already configured.");
163
- // Declaring the option is declaring a limit, as for the guards option
164
- // and an API's own `rateLimit: {}`. An empty map would configure a
165
- // limiter with nothing to enforce, and every API naming a policy would
166
- // be reported as if the option were missing: the wrong line to fix.
167
- if (Object.keys(config.policies).length === 0) {
168
- throw new Error("Lambder: the rateLimits option was declared with no policies in it, which configures nothing. Name the policies APIs will declare, or leave the option off.");
169
- }
170
- for (const [name, policy] of Object.entries(config.policies)) {
171
- const per = policy.per;
172
- if (per !== undefined && per !== "ip" && per !== "session" && typeof per?.handler !== "function") {
173
- throw new Error(`Lambder: rate-limit policy "${name}" has a per that is not "ip", "session", or a { apiInput?, handler } key. Leave per out for a policy the handler charges with its own key.`);
174
- }
175
- if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
176
- throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
177
- }
178
- assertWindowLimits(`rate-limit policy "${name}"`, policy);
179
- const chargeAt = policy.chargeAt;
180
- if (chargeAt !== undefined) {
181
- if (chargeAt !== "beforeGuards" && chargeAt !== "afterGuards") {
182
- throw new Error(`Lambder: rate-limit policy "${name}" has chargeAt "${String(chargeAt)}"; use "afterGuards" (default) or "beforeGuards".`);
183
- }
184
- if (per === undefined || per === "ip" || per === "session") {
185
- throw new Error(`Lambder: rate-limit policy "${name}" sets chargeAt, which only a policy keyed by a { apiInput?, handler } key takes: per "ip" and per "session" each run at one fixed place, and a policy without per is charged by the code that names it.`);
186
- }
187
- }
188
- assertRateLimitMessage(`rate-limit policy "${name}"`, policy.refusal);
189
- const budget = policy.budget;
190
- if (budget !== undefined && budget !== "perApi" && budget !== "perPolicy") {
191
- throw new Error(`Lambder: rate-limit policy "${name}" has budget "${String(budget)}"; use "perApi" (default: each referencing API counts separately) or "perPolicy" (one counter shared by every referencing API).`);
192
- }
202
+ // Declaring the policies is declaring a limit, as for the guards
203
+ // option and an API's own `rateLimit: {}`. An empty map would
204
+ // configure a limiter with nothing to enforce, and every API naming a
205
+ // policy would be reported as if the option were missing: the wrong
206
+ // line to fix.
207
+ if (config.policies !== undefined && Object.keys(config.policies).length === 0) {
208
+ throw new Error("Lambder: the rateLimits option was declared with no policies in it, which configures nothing. Name the policies APIs will declare, or leave the policies out where the app's modules declare them all.");
193
209
  }
210
+ for (const [name, policy] of Object.entries(config.policies ?? {}))
211
+ assertRateLimitPolicyDeclaration(name, policy);
194
212
  const ipv6PrefixLength = config.ipv6PrefixLength ?? DEFAULT_IPV6_RATE_LIMIT_PREFIX;
195
213
  if (!Number.isInteger(ipv6PrefixLength) || ipv6PrefixLength < 1 || ipv6PrefixLength > 128) {
196
214
  throw new Error(`Lambder: rateLimits.ipv6PrefixLength must be a whole number from 1 to 128, got ${String(ipv6PrefixLength)}.`);
@@ -198,7 +216,24 @@ export class LambderApiRateLimitsEngine {
198
216
  this.limiter = config.limiter;
199
217
  this.failOpen = config.failOpen ?? true;
200
218
  this.ipv6PrefixLength = ipv6PrefixLength;
201
- this.policies = new Map(Object.entries(config.policies));
219
+ this.policies = new Map(Object.entries(config.policies ?? {}));
220
+ }
221
+ /**
222
+ * Adds a module's policies beside the ones declared so far, counted on
223
+ * the limiter given at creation. Its names were checked against them
224
+ * before (LambderApiPipeline.modulesToAdd), so a name met again here is a
225
+ * caller that skipped that check.
226
+ */
227
+ add(policies) {
228
+ if (!this.limiter)
229
+ throw new Error("Lambder: rate-limit policies were added before a limiter was configured.");
230
+ for (const [name, policy] of Object.entries(policies)) {
231
+ if (this.policies.has(name))
232
+ throw new Error(`Lambder: rate-limit policy "${name}" is already defined.`);
233
+ assertRateLimitPolicyDeclaration(name, policy);
234
+ }
235
+ for (const [name, policy] of Object.entries(policies))
236
+ this.policies.set(name, policy);
202
237
  }
203
238
  /**
204
239
  * Puts the engine over another limiter, for `lambder/testing`; the named
@@ -211,8 +246,12 @@ export class LambderApiRateLimitsEngine {
211
246
  this.limiter = limiter;
212
247
  return true;
213
248
  }
214
- /** Startup validation of one API registration's rateLimit option. */
215
- assertRegistration(apiName, mode, rateLimitOption) {
249
+ /**
250
+ * Startup validation of one API registration's rateLimit option.
251
+ * `adding` holds the policies of the modules the same registration adds,
252
+ * which the engine takes only once every endpoint of it passed.
253
+ */
254
+ assertRegistration(apiName, mode, rateLimitOption, adding) {
216
255
  const entries = toRateLimitEntries(rateLimitOption);
217
256
  // Declaring the option is declaring a limit, as for guards: `{}`,
218
257
  // `[]` and `{ name: undefined }` would register an API that
@@ -222,9 +261,9 @@ export class LambderApiRateLimitsEngine {
222
261
  `Name the policy that limits it, or omit the option entirely.`);
223
262
  }
224
263
  for (const { name, override } of entries) {
225
- const policy = this.policies.get(name);
264
+ const policy = this.policies.get(name) ?? (adding && Object.prototype.hasOwnProperty.call(adding, name) ? adding[name] : undefined);
226
265
  if (!policy) {
227
- throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation.`);
266
+ throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation, or, on a server, in the module the API is declared with (defineApiModule).`);
228
267
  }
229
268
  if (policy.per === undefined) {
230
269
  throw new Error(`Lambder: API "${apiName}" references rate-limit policy "${name}", which declares no per: its key is the one a handler passes to ctx.rateLimit("${name}", key), so the request alone cannot be counted against it.`);
@@ -12,21 +12,21 @@ import { LambderFiles } from "./LambderFiles.js";
12
12
  import { type LambderPipelineBackends, type LambderPipelineBackendSwap } from "../api/LambderApiPipeline.js";
13
13
  import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
14
14
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
15
- import { type LambderWireRefusalsOf, type LambderHandlerRefusalsOf, type LambderRefusalNamesIn, type LambderRefusalsOption, type LambderRefusalVocabularyOption, type LambderRefusalVocabularyOptionChecks, type LambderMergedRefusalVocabulary } from "../api/LambderApiRefusals.js";
15
+ import { type LambderHandlerRefusalsOf, type LambderRefusalVocabularyOption, type LambderRefusalVocabularyOptionChecks, type LambderMergedRefusalVocabulary } from "../api/LambderApiRefusals.js";
16
16
  import { type LambderDeclaredRefuse } from "../shared/wire/LambderApiRefusal.js";
17
17
  import { type LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
18
18
  import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
19
19
  import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
20
- import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
21
- import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf } from "../api/LambderApiGuards.js";
22
- import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
20
+ import type { LambderApiGuard, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
21
+ import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
23
22
  import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
24
- import type { LambderContractEntry, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
25
- import { type LambderActionNamesCheck, type LambderApiDeclaration, type LambderApiDeclarations, type LambderApiGroup, type LambderApiModeOf, type LambderAppTypes, type LambderContractOfGroups, type LambderGroupNameCheck, type LambderGroupPartsCheck, type LambderMergedParts, type LambderLazyApiGroup, type LambderPlainAppTypes, type LambderPublicSessionPolicyCheck, type LambderRegisteredGroupsCheck, type LambderRegistrableApiGroup, type LambderSessionModeCheck } from "../api/LambderApiDeclarations.js";
23
+ import { type LambderAppTypes, type LambderContractOfGroups, type LambderPlainAppTypes, type LambderRegisteredGroupsCheck, type LambderRegistrableApiGroup } from "../api/LambderApiDeclarations.js";
24
+ import { type LambderDefineApi, type LambderDefineApiGroup, type LambderDefineLazyApiGroup } from "./LambderApiBuilders.js";
25
+ import { LambderApiModule, type LambderApiModuleOptionChecks, type LambderApiModuleOptions, type LambderModuleAppTypes } from "./LambderApiModule.js";
26
26
  import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
27
- import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
27
+ import type { LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
28
28
  import { type LambderCallSummary } from "./LambderCallSummary.js";
29
- import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderObjectOutputCheck, type LambderPayloadSliceCheck, type LambderRequirableGuardsField, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
29
+ 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 LambderNoExtraKeys, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
30
30
  /** Everything `lambder/testing` may put under a built instance: the pipeline's stores, and the source its files are read from. */
31
31
  export type LambderInstanceBackends = LambderPipelineBackends & {
32
32
  fileSource?: LambderFileSource;
@@ -44,17 +44,6 @@ export type LambderInstanceBackendSwap = LambderPipelineBackendSwap & {
44
44
  * class cannot be read without it.
45
45
  */
46
46
  export type LambderCreatedHook = (lambderInstance: Lambder<any, any>) => void | Promise<void>;
47
- /** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
48
- type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
49
- /**
50
- * What an endpoint's handler is handed: the session context when one of its
51
- * guards needs a session (so ctx.session is typed present), the plain one
52
- * otherwise, with the payload, the guards' data and a refuse() typed to the
53
- * codes the endpoint may refuse with.
54
- */
55
- type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt, TRefusalsOpt> = (LambderApiModeOf<TApp["guards"], TGuardsOpt> extends "session" ? LambderSessionRenderContext<TPayload, TApp["session"], Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["policies"]> : LambderRenderContext<TPayload, Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["session"], TApp["policies"]>) & {
56
- refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
57
- };
58
47
  /**
59
48
  * Main Lambder class for building type-safe serverless APIs. Create
60
49
  * instances with initLambder<SessionData>().create({...}) (see below): the
@@ -117,10 +106,15 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
117
106
  private readonly apiGroupNames;
118
107
  /** The lazy groups not loaded yet, each with its load in progress, if one is. */
119
108
  private readonly lazyApiGroups;
120
- /** 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. */
121
- private readonly guards;
122
- /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
123
- private readonly rateLimitPolicies;
109
+ /**
110
+ * The guards given at creation merged into one map, and every registered
111
+ * module's beside them, kept for registration (a guard's session and
112
+ * refusal codes), apiSignatures() (a guard's schema is part of the
113
+ * signature of every endpoint declaring it) and apiOptionEntries().
114
+ */
115
+ private guards;
116
+ /** The rate-limit policies given at creation, and every registered module's beside them, kept for apiOptionEntries(), which records each one less its key handler. */
117
+ private rateLimitPolicies;
124
118
  /** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
125
119
  private readonly refusalVocabulary;
126
120
  /** Every API's refusals option as written, for apiOptionEntries(); its definition holds the resolved set. */
@@ -186,73 +180,50 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
186
180
  * res.templateFile (markers optional) with no-cache.
187
181
  */
188
182
  serveIndexHtml(handler?: LambderFallbackHandler, options?: LambderIndexHtmlOptions): this;
189
- addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
183
+ addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) => LambderMaybePromise<LambderResponse>): this;
190
184
  addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
191
- addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<TApp["sessions"]>): this;
185
+ addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) => LambderMaybePromise<LambderResponse>) & LambderSessionEnabledInstance<TApp["sessions"]>): this;
192
186
  addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TApp["session"]> & LambderSessionEnabledInstance<TApp["sessions"]>): this;
193
187
  /**
194
- * Declares one endpoint, typed on this instance's types: its input and
195
- * output schemas, its guards, rate limits, idempotency and refusals, and
196
- * its handler. Nothing is registered until the declaration is put in a
197
- * group (defineApiGroup) and the group is registered (registerApiGroups).
188
+ * Declares one endpoint, typed on this instance's types (see
189
+ * LambderDefineApi). Nothing is registered until the declaration is put
190
+ * in a group (defineApiGroup) and the group is registered
191
+ * (registerApiGroups).
198
192
  *
199
- * The guards say who may call it, and so its mode: an endpoint whose
200
- * guards include one that needs a session (`session: true`) is a session
201
- * endpoint, its session read before the guards run and a call without
202
- * one answered sessionExpired, and its handler's ctx.session is typed
203
- * present. Any other endpoint is public.
193
+ * An arrow property rather than a method, as are the two below, so an
194
+ * app can hand them out detached; handed out under the instance's name
195
+ * (`export const defineApi: AppLambder["defineApi"] = lambderApp.defineApi`),
196
+ * a declaration file names each by the instance's type rather than
197
+ * writing the app's types out again in each.
198
+ */
199
+ readonly defineApi: LambderDefineApi<TApp>;
200
+ /** Gathers endpoints into a named group (see LambderDefineApiGroup). */
201
+ readonly defineApiGroup: LambderDefineApiGroup;
202
+ /** A group loaded on the first call to one of its endpoints (see LambderDefineLazyApiGroup). */
203
+ readonly lazyApiGroup: LambderDefineLazyApiGroup;
204
+ /**
205
+ * Defines a module: guards and rate-limit policies a part of the app
206
+ * declares for its own endpoints, beside the ones this instance was
207
+ * created with. Returns the module's builders, whose endpoints may name
208
+ * the instance's guards and policies and the module's own, and nothing
209
+ * else; their groups are registered with registerApiGroups() as any
210
+ * are, and their registration adds the module's guards and policies to
211
+ * the instance that registers them.
204
212
  *
205
- * An arrow property rather than a method, so an app can hand it out
206
- * detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
207
- */
208
- readonly defineApi: <TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<TApp["policies"]> = never, const TGuardsOpt extends LambderGuardsOption<TApp["guards"]> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<TApp["refusals"]> = never>(options: {
209
- input: TInput;
210
- output: TOutput;
211
- } & {
212
- /** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. A per-session policy needs one of the endpoint's guards to need a session. */
213
- rateLimit?: TRateOpt;
214
- /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
215
- idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
216
- /**
217
- * The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
218
- * non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
219
- * its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
220
- */
221
- refusals?: TRefusalsOpt;
222
- /**
223
- * Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
224
- * body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
225
- * compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
226
- * cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
227
- * both ends. A transport setting of this server's, not part of the API's contract.
228
- */
229
- compress?: boolean | "auto";
230
- } & LambderRequirableGuardsField<TApp["guardsRequired"], TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<TApp["guards"], TApp["policies"], TGuardsOpt, TRateOpt, z.input<TInput>> & LambderPublicSessionPolicyCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["policies"], TRateOpt> & LambderSessionModeCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["sessions"]>,
231
- /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
232
- handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => MaybePromise<TAnswer>) => LambderApiDeclaration<LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, LambderApiModeOf<TApp["guards"], TGuardsOpt>, LambderGuardInputsOf<TApp["guards"], TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>>>;
233
- /**
234
- * Gathers endpoints into a named group: each is registered as
235
- * `name.action` and called at `{apiPath}/{name}/{action}`, and every
236
- * Lambder caller reaches it as `caller.name.action(input)`. A group's
237
- * name and its actions are identifiers, and a group may not take a name
238
- * a caller already answers for (see LAMBDER_RESERVED_GROUP_NAMES).
213
+ * ```typescript
214
+ * export const giftCardModule = storeApp.defineApiModule({
215
+ * guards: { cardHolder },
216
+ * rateLimitPolicies: { redeemPerCard: { perHour: 5, per: "session" } },
217
+ * });
218
+ * ```
239
219
  *
240
- * A group declared across files takes each file's part:
241
- * `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action
242
- * two parts declare is refused, at compile time and at startup, rather
243
- * than one silently replacing the other as an object spread would.
220
+ * A name the instance declares is refused here, at compile time and at
221
+ * the call; a name two modules declare is refused when the second is
222
+ * registered, as create() refuses a name two maps declare. A module's
223
+ * rate-limit policies count on the instance's limiter, so they need the
224
+ * rateLimits option at creation, whose own policies may then be left out.
244
225
  */
245
- readonly defineApiGroup: <const TName extends string, const TParts extends readonly [LambderApiDeclarations, ...LambderApiDeclarations[]]>(name: TName & LambderGroupNameCheck<TName>, ...parts: TParts & LambderGroupPartsCheck<TParts> & { [TIndex in keyof TParts]: LambderActionNamesCheck<TParts[TIndex]>; }) => LambderApiGroup<TName, LambderMergedParts<TParts>>;
246
- /**
247
- * A group loaded on the first call to one of its endpoints:
248
- * `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
249
- * A cold start then parses none of it, nor anything only it imports,
250
- * until a request calls it; the contract is the loaded group's, read off
251
- * its type. The group loaded must carry the same name, which the type
252
- * checks and loading checks again. `loadApiGroups()` loads every group,
253
- * for a build step or a boot check that has to see them all.
254
- */
255
- readonly lazyApiGroup: <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
226
+ defineApiModule<const TOptions extends LambderApiModuleOptions<TApp["session"]>>(options: LambderNoExtraKeys<TOptions, LambderApiModuleOptions<TApp["session"]>> & LambderApiModuleOptionChecks<TApp, TOptions>): LambderApiModule<LambderModuleAppTypes<TApp, LambderGivenOption<TOptions, "guards">, LambderGivenOption<TOptions, "rateLimitPolicies">>>;
256
227
  /**
257
228
  * Registers groups of endpoints, as one contract: `typeof
258
229
  * lambder.ApiContract` then maps every `group.action` to its entry, one
@@ -299,7 +270,12 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
299
270
  * group answers each of them as an eager one does.
300
271
  */
301
272
  private registeredDefinitionOf;
302
- /** The endpoints of one group, each checked as `group.action`; nothing is registered until they are committed. */
273
+ /**
274
+ * The endpoints of one group, each checked as `group.action` against the
275
+ * instance's guards and policies and those `adding` holds, of the modules
276
+ * the same registration adds; nothing is registered until they are
277
+ * committed.
278
+ */
303
279
  private prepareApiGroup;
304
280
  /**
305
281
  * A lazy group, loaded once: its import, checked to be the group it was
@@ -337,8 +313,8 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
337
313
  * Use a type-guard filter to get a typed event:
338
314
  * `(event): event is ScheduledEvent => ...`
339
315
  */
340
- addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => MaybePromise<unknown>): this;
341
- addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => MaybePromise<unknown>): this;
316
+ addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => LambderMaybePromise<unknown>): this;
317
+ addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => LambderMaybePromise<unknown>): this;
342
318
  /**
343
319
  * Hands the instance to a function that registers on it (routes, hooks,
344
320
  * actions) and continues the chain with what it returns. Endpoints are
@@ -701,4 +677,3 @@ export declare const initLambder: <TSessionData = any>() => {
701
677
  guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, {}, false>;
702
678
  rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
703
679
  };
704
- export {};