lambder 11.0.1 → 12.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 (56) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +18 -13
  3. package/dist/api/LambderApiDeclarations.d.ts +194 -0
  4. package/dist/api/LambderApiDeclarations.js +52 -0
  5. package/dist/api/LambderApiDefinition.d.ts +1 -1
  6. package/dist/api/LambderApiGuards.d.ts +21 -22
  7. package/dist/api/LambderApiGuards.js +1 -1
  8. package/dist/api/LambderApiRateLimits.d.ts +19 -19
  9. package/dist/api/LambderApiRateLimits.js +1 -1
  10. package/dist/api/LambderApiRequest.d.ts +15 -6
  11. package/dist/api/LambderApiRequest.js +8 -9
  12. package/dist/build/generatedTables.d.ts +2 -1
  13. package/dist/build/generatedTables.js +1 -1
  14. package/dist/client/LambderCaller.d.ts +36 -23
  15. package/dist/client/LambderCaller.js +88 -67
  16. package/dist/client/lambderFetchTransport.js +2 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.js +2 -0
  19. package/dist/core/Lambder.d.ts +203 -87
  20. package/dist/core/Lambder.js +265 -86
  21. package/dist/core/LambderContext.d.ts +1 -1
  22. package/dist/core/LambderContext.js +13 -5
  23. package/dist/core/LambderCreateOptions.d.ts +28 -39
  24. package/dist/core/LambderCreateOptions.js +2 -3
  25. package/dist/core/LambderPublicFiles.d.ts +8 -1
  26. package/dist/core/LambderPublicFiles.js +1 -1
  27. package/dist/index.d.ts +7 -3
  28. package/dist/index.js +2 -0
  29. package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
  30. package/dist/invoke/LambderInvokeCaller.js +110 -94
  31. package/dist/invoke/LambderLambdaEvent.d.ts +0 -1
  32. package/dist/invoke/LambderLambdaEvent.js +0 -1
  33. package/dist/invoke/lambderHandlerTransport.js +2 -1
  34. package/dist/mock/LambderMockApp.d.ts +31 -25
  35. package/dist/mock/LambderMockApp.js +52 -45
  36. package/dist/mock/LambderMockCreateOptions.d.ts +6 -7
  37. package/dist/mock/LambderMockTypes.d.ts +44 -7
  38. package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
  39. package/dist/mock/lambderMockInvokeTransport.js +15 -7
  40. package/dist/mock/lambderMockMswHandler.js +8 -4
  41. package/dist/mock.d.ts +2 -2
  42. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  43. package/dist/shared/transport/LambderApiTransport.js +2 -3
  44. package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
  45. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  46. package/dist/shared/wire/LambderApiContract.d.ts +4 -18
  47. package/dist/shared/wire/LambderApiContract.js +1 -1
  48. package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
  49. package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
  50. package/dist/shared/wire/LambderApiNames.d.ts +55 -0
  51. package/dist/shared/wire/LambderApiNames.js +81 -0
  52. package/dist/shared/wire/LambderUploadSchemas.js +2 -1
  53. package/dist/testing/LambderTestApp.d.ts +5 -1
  54. package/dist/testing/LambderTestVisitor.d.ts +16 -8
  55. package/dist/testing/LambderTestVisitor.js +33 -25
  56. package/package.json +1 -1
@@ -21,7 +21,8 @@ import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptio
21
21
  import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf } from "../api/LambderApiGuards.js";
22
22
  import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
23
23
  import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
24
- import type { LambderContractEntry, LambderJsonOf, LambderMergeContract } from "../shared/wire/LambderApiContract.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";
25
26
  import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
26
27
  import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
27
28
  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";
@@ -39,36 +40,41 @@ export type LambderInstanceBackendSwap = LambderPipelineBackendSwap & {
39
40
  * because its parameter is the class, and an options module that names the
40
41
  * class cannot be read without it.
41
42
  */
42
- export type LambderCreatedHook = (lambderInstance: Lambder<any, any, any, any, any, any, any, any, any, any>) => void | Promise<void>;
43
+ export type LambderCreatedHook = (lambderInstance: Lambder<any, any>) => void | Promise<void>;
43
44
  /** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
44
45
  type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
46
+ /**
47
+ * What an endpoint's handler is handed: the session context when one of its
48
+ * guards needs a session (so ctx.session is typed present), the plain one
49
+ * otherwise, with the payload, the guards' data and a refuse() typed to the
50
+ * codes the endpoint may refuse with.
51
+ */
52
+ 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"]>) & {
53
+ refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
54
+ };
45
55
  /**
46
56
  * Main Lambder class for building type-safe serverless APIs. Create
47
57
  * instances with initLambder<SessionData>().create({...}) (see below): the
48
58
  * whole configuration, including the typed policy layer, is given at
49
- * construction, and only registration (routes, apis, hooks, use) chains.
59
+ * construction. Endpoints are declared as values on the instance
60
+ * (defineApi, defineApiGroup) and registered in one registerApiGroups()
61
+ * call; routes, hooks and actions chain.
50
62
  *
51
- * @typeParam TSessionData - Type of session data stored in DynamoDB
52
- * @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
53
- * @typeParam _TRateLimitPolicies - @internal Inferred from create()'s rateLimits.policies (do not pass manually)
54
- * @typeParam _TGuards - @internal Guard metadata map inferred from create()'s guards (do not pass manually)
55
- * @typeParam _TIdempotencyEnabled - @internal True when create() received idempotency (do not pass manually)
56
- * @typeParam _TSessionGuardsRequired - @internal True when create() received requireSessionApiGuards (do not pass manually)
57
- * @typeParam _TPublicGuardsRequired - @internal True when create() received requirePublicApiGuards (do not pass manually)
58
- * @typeParam _TSessionsEnabled - @internal True when create() received the session option (do not pass manually). Defaults to true, unlike its siblings, so a plugin annotating its parameter as the bare Lambder<SessionData> can still register session APIs. create() knows the option and supplies the false; `new Lambder(...)` relies on the registration-time throw alone.
59
- * @typeParam _TRefusals - @internal The refusal vocabulary declareRefusals() gave the init (do not pass manually)
60
- * @typeParam _TRefusalCodesRequired - @internal True when declareRefusals() received requireCodes (do not pass manually)
63
+ * @typeParam TApp - @internal What create() configured, as LambderAppTypes: the session data, policies, guards, idempotency, the guard requirement, sessions and the refusal vocabulary (do not pass manually)
64
+ * @typeParam _TContract - @internal The contract of the groups registered so far (do not pass manually)
61
65
  *
62
66
  * @example
63
67
  * ```typescript
64
68
  * interface SessionData { userId: string; role: string; }
65
69
  *
66
- * const lambder = initLambder<SessionData>().create({ apiPath: '/api' })
67
- * .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
68
- * .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
70
+ * const app = initLambder<SessionData>().create({ apiPath: '/api', session, guards });
71
+ * const userApis = app.defineApiGroup("users", {
72
+ * get: app.defineApi({ input: z.object({...}), output: z.object({...}), guards: "signedIn" }, handler),
73
+ * });
74
+ * const lambder = app.registerApiGroups(userApis);
69
75
  * ```
70
76
  */
71
- export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}, _TRateLimitPolicies extends Record<string, LambderApiRateLimitPolicyConfig> = {}, _TGuards extends Record<string, any> = {}, _TIdempotencyEnabled extends boolean = false, _TSessionGuardsRequired extends boolean = false, _TPublicGuardsRequired extends boolean = false, _TSessionsEnabled extends boolean = true, _TRefusals extends LambderRefusalVocabulary = {}, _TRefusalCodesRequired extends boolean = false> {
77
+ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes, _TContract extends Record<string, any> = {}> {
72
78
  apiPath: string;
73
79
  /** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
74
80
  apiVersion: null | string;
@@ -78,24 +84,35 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
78
84
  * Type property for extracting the API contract: every registered API's
79
85
  * input, output, mode and declared options, as a client calls it.
80
86
  *
81
- * A small app's client imports it as it is. It is an intersection one
82
- * member deep per endpoint, so reading it generically costs a large app's
83
- * client most of its type check; writeApiContract (lambder/build) reads
84
- * this property off the exported instance and writes the contract out as
85
- * plain types for such a client to import instead.
87
+ * A small app's client imports it as it is. A large app's client reads
88
+ * the plain types writeApiContract (lambder/build) prints from this
89
+ * property instead, which cost it nothing to resolve.
86
90
  *
87
91
  * @example
88
92
  * ```typescript
89
- * export const lambder = initLambder().create({ ... }).addApi(...).addApi(...);
93
+ * export const lambder = lambderApp.registerApiGroups(userApis, orderApis);
90
94
  * export type ApiContractType = typeof lambder.ApiContract;
91
95
  * ```
92
96
  */
93
97
  readonly ApiContract: _TContract;
98
+ /**
99
+ * Type property for what create() configured (LambderAppTypes): the
100
+ * session data, policies, guards and the rest, for code that takes any
101
+ * instance and needs one of them (lambderTestApp reads the session data
102
+ * type here). Like ApiContract, it has no value.
103
+ */
104
+ readonly AppTypes: TApp;
94
105
  private actionList;
95
106
  /** The API core: the pipeline every API call runs through, shared in shape with the mock runtime. */
96
107
  private readonly pipeline;
97
- /** Every registered API by name: the duplicate-name check, and what apiSignatures() digests. */
108
+ /** Every registered API by name: what dispatch, apiSignatures() and apiOptionEntries() read. */
98
109
  private readonly apiDefinitions;
110
+ /** What answers each registered API: its output schema, compression setting and handler. */
111
+ private readonly apiHandlers;
112
+ /** Every group name registered, lazy or not: the duplicate-group check. */
113
+ private readonly apiGroupNames;
114
+ /** The lazy groups not loaded yet, each with its load in progress, if one is. */
115
+ private readonly lazyApiGroups;
99
116
  /** The guards given at creation, merged into one map, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
100
117
  private readonly guards;
101
118
  /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
@@ -117,7 +134,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
117
134
  private eventActionList;
118
135
  private corsConfig;
119
136
  private finalizeOptions;
120
- private requireSessionApiGuards;
137
+ /** Whether every endpoint has to declare guards (create()'s requireApiGuards). */
138
+ private readonly requireApiGuards;
121
139
  /** Whether every refusal an API answers with has to name a code (declareRefusals's requireCodes). */
122
140
  private readonly requireRefusalCodes;
123
141
  /** Told what a request threw, beside whatever answers it; null outside a test. See LAMBDER_CRASH_WATCH. */
@@ -128,8 +146,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
128
146
  private readonly contextTools;
129
147
  private readonly trustedClientIpHeaders;
130
148
  private readonly trustedHostHeaders;
131
- private requirePublicApiGuards;
132
- constructor(given?: LambderCreateOptions<TSessionData>);
149
+ constructor(given?: LambderCreateOptions<TApp["session"]>);
133
150
  setRouteFallbackHandler(routeFallbackHandler: LambderFallbackHandler): this;
134
151
  setApiFallbackHandler(apiFallbackHandler: LambderFallbackHandler): this;
135
152
  setApiInputValidationErrorHandler(apiInputValidationErrorHandler: LambderInputValidationHandler): this;
@@ -163,45 +180,33 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
163
180
  * res.templateFile (markers optional) with no-cache.
164
181
  */
165
182
  serveIndexHtml(handler?: LambderFallbackHandler, options?: LambderIndexHtmlOptions): this;
166
- addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TSessionData, _TRateLimitPolicies>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
183
+ addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
167
184
  addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
168
- addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TSessionData, LambderPathParamsOf<TPath>, {}, _TRateLimitPolicies>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
169
- addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TSessionData> & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
170
- addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, false> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
171
- input: TInput;
172
- output: TOutput;
173
- } & {
174
- /** 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. */
175
- rateLimit?: TRateOpt;
176
- /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
177
- idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
178
- /**
179
- * The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
180
- * non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
181
- * its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
182
- */
183
- refusals?: TRefusalsOpt;
184
- /**
185
- * Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
186
- * body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
187
- * compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
188
- * cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
189
- * both ends. A transport setting of this server's, not part of the API's contract.
190
- */
191
- compress?: boolean | "auto";
192
- } & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<_TGuards, _TRateLimitPolicies, TGuardsOpt, TRateOpt, z.input<TInput>>,
193
- /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
194
- handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, TSessionData, _TRateLimitPolicies> & {
195
- refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>, _TRefusalCodesRequired>;
196
- }) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, "public", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled, _TRefusals, _TRefusalCodesRequired>;
197
- addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
185
+ addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<TApp["sessions"]>): this;
186
+ addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TApp["session"]> & LambderSessionEnabledInstance<TApp["sessions"]>): this;
187
+ /**
188
+ * Declares one endpoint, typed on this instance's types: its input and
189
+ * output schemas, its guards, rate limits, idempotency and refusals, and
190
+ * its handler. Nothing is registered until the declaration is put in a
191
+ * group (defineApiGroup) and the group is registered (registerApiGroups).
192
+ *
193
+ * The guards say who may call it, and so its mode: an endpoint whose
194
+ * guards include one that needs a session (`session: true`) is a session
195
+ * endpoint, its session read before the guards run and a call without
196
+ * one answered sessionExpired, and its handler's ctx.session is typed
197
+ * present. Any other endpoint is public.
198
+ *
199
+ * An arrow property rather than a method, so an app can hand it out
200
+ * detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
201
+ */
202
+ 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: {
198
203
  input: TInput;
199
204
  output: TOutput;
200
205
  } & {
201
- /** 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. */
206
+ /** 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. */
202
207
  rateLimit?: TRateOpt;
203
208
  /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
204
- idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
209
+ idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
205
210
  /**
206
211
  * The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
207
212
  * non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
@@ -216,19 +221,96 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
216
221
  * both ends. A transport setting of this server's, not part of the API's contract.
217
222
  */
218
223
  compress?: boolean | "auto";
219
- } & LambderRequirableGuardsField<_TSessionGuardsRequired, TGuardsOpt> & LambderSessionEnabledInstance<_TSessionsEnabled> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<_TGuards, _TRateLimitPolicies, TGuardsOpt, TRateOpt, z.input<TInput>>,
224
+ } & 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"]>,
220
225
  /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
221
- handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, _TRateLimitPolicies> & {
222
- refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>, _TRefusalCodesRequired>;
223
- }) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, "session", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled, _TRefusals, _TRefusalCodesRequired>;
226
+ 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>>>>;
227
+ /**
228
+ * Gathers endpoints into a named group: each is registered as
229
+ * `name.action` and called at `{apiPath}/{name}/{action}`, and every
230
+ * Lambder caller reaches it as `caller.name.action(input)`. A group's
231
+ * name and its actions are identifiers, and a group may not take a name
232
+ * a caller already answers for (see LAMBDER_RESERVED_GROUP_NAMES).
233
+ *
234
+ * A group declared across files takes each file's part:
235
+ * `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action
236
+ * two parts declare is refused, at compile time and at startup, rather
237
+ * than one silently replacing the other as an object spread would.
238
+ */
239
+ 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>>;
240
+ /**
241
+ * A group loaded on the first call to one of its endpoints:
242
+ * `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
243
+ * A cold start then parses none of it, nor anything only it imports,
244
+ * until a request calls it; the contract is the loaded group's, read off
245
+ * its type. The group loaded must carry the same name, which the type
246
+ * checks and loading checks again. `loadApiGroups()` loads every group,
247
+ * for a build step or a boot check that has to see them all.
248
+ */
249
+ readonly lazyApiGroup: <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
250
+ /**
251
+ * Registers groups of endpoints, as one contract: `typeof
252
+ * lambder.ApiContract` then maps every `group.action` to its entry, one
253
+ * flat object type the api-contract generator prints as it is. Register
254
+ * every group in one call where you can: the contract of one call is one
255
+ * mapped type, and a second call intersects it with the first.
256
+ *
257
+ * The call's groups take their place in the first-match chain here, as
258
+ * a route does: a route or action registered before them sees their calls
259
+ * first, one registered after them never does. The beforeRender hooks run
260
+ * before a lazy group loads, so a hook that answers a request spares it
261
+ * the import; one that throws a refusal or charges a per-API budget loads
262
+ * it (see registeredDefinitionOf).
263
+ *
264
+ * Refused at compile time: a group typed `any`, or a lazy one loading a
265
+ * group typed `any`, or a group whose endpoints are (its endpoints would
266
+ * be `any` to every client), and a group name given twice. Refused at
267
+ * registration, which is at startup for a group and at its first call for
268
+ * a lazy one: everything registration refuses of one endpoint (an unknown
269
+ * guard or policy, a missing guard where requireApiGuards is on, a
270
+ * refusal code outside the vocabulary).
271
+ */
272
+ registerApiGroups<const TGroups extends readonly LambderRegistrableApiGroup[]>(this: LambderRegisteredGroupsCheck<TGroups, Lambder<TApp, _TContract>>, ...groups: TGroups): Lambder<TApp, _TContract & LambderContractOfGroups<TGroups>>;
273
+ /**
274
+ * Loads every lazy group not loaded yet, registering its endpoints: what
275
+ * a build step digesting the signatures, a generator writing the options
276
+ * or a boot check has to do before it sees every endpoint. A group whose
277
+ * registration fails rejects here, which is the point of calling it.
278
+ */
279
+ loadApiGroups(): Promise<void>;
224
280
  /**
225
- * What registering an API is, for addApi and addSessionApi alike: the
226
- * checks that can refuse it, then its definition recorded (what
227
- * apiSignatures() digests) and its action appended to the first-match
228
- * chain. The two public methods differ only in the mode and in the types
229
- * they give the handler.
281
+ * An API call to a registered group, its hooks run: its group loaded
282
+ * first when it is a lazy one no call has reached yet, then its endpoint
283
+ * run. An action the group does not have is answered as any unmatched API
284
+ * call is, fallback hooks first.
230
285
  */
231
- private registerApi;
286
+ private dispatchApiCall;
287
+ /**
288
+ * The definition of the endpoint a call names, its group loaded first
289
+ * when it is a lazy one no call has reached yet; undefined for a name no
290
+ * registered group declares. Everything that reads an endpoint's
291
+ * declarations for a call asks here (its dispatch, a refusal a hook
292
+ * throws on the way to it, a per-API budget a hook charges), so a lazy
293
+ * group answers each of them as an eager one does.
294
+ */
295
+ private registeredDefinitionOf;
296
+ /** The endpoints of one group, each checked as `group.action`; nothing is registered until they are committed. */
297
+ private prepareApiGroup;
298
+ /**
299
+ * A lazy group, loaded once: its import, checked to be the group it was
300
+ * registered as, then registered. Concurrent first calls share one load;
301
+ * a failed load is forgotten, so the next call tries again rather than
302
+ * answering every later call with the first failure.
303
+ */
304
+ private loadLazyApiGroup;
305
+ /**
306
+ * What registering an endpoint takes: the checks that can refuse it and
307
+ * its mode read off its guards, into the definition apiSignatures()
308
+ * digests, beside its handler. Nothing is recorded here (commitApi does
309
+ * that), so a refusal anywhere in a registration leaves nothing behind.
310
+ */
311
+ private prepareApi;
312
+ /** Records a checked endpoint: its definition, its refusals option as written, and what answers it. */
313
+ private commitApi;
232
314
  addHook(hookEvent: 'created', hookFn: LambderCreatedHook, priority?: number): this;
233
315
  addHook(hookEvent: 'beforeRender', hookFn: LambderBeforeRenderHook, priority?: number): this;
234
316
  addHook(hookEvent: 'afterRender', hookFn: LambderAfterRenderHook, priority?: number): this;
@@ -251,7 +333,13 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
251
333
  */
252
334
  addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => MaybePromise<unknown>): this;
253
335
  addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => MaybePromise<unknown>): this;
254
- use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract, any, any, any, any, any, any, any, any>) => Lambder<TSessionData, _TNewContract, any, any, any, any, any, any, any, any>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract), _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled, _TRefusals, _TRefusalCodesRequired>;
336
+ /**
337
+ * Hands the instance to a function that registers on it (routes, hooks,
338
+ * actions) and continues the chain with what it returns. Endpoints are
339
+ * not registered this way: they are values, registered by
340
+ * registerApiGroups(), so a plugin adds nothing to the contract.
341
+ */
342
+ use(plugin: (lambder: this) => unknown): this;
255
343
  /**
256
344
  * A session controller for a context: what creates, rotates, refreshes
257
345
  * and ends sessions. An API call presents its posted CSRF token; a route
@@ -259,9 +347,9 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
259
347
  * carries one as `ctx.sessionController`; this is for a context it did
260
348
  * not render, such as one createContext() built from an event on its own.
261
349
  */
262
- getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>;
350
+ getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any, TApp["session"]>): LambderSessionController<TApp["session"]>;
263
351
  /** The session manager, for code that works on sessions outside a request (maintenance, tests). */
264
- getSessionManager(): LambderSessionManager<TSessionData>;
352
+ getSessionManager(): LambderSessionManager<TApp["session"]>;
265
353
  /**
266
354
  * The backend swap `lambder/testing` performs: the stores given go under
267
355
  * this instance in place, so every handler and guard that closed over it
@@ -316,7 +404,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
316
404
  * same object. Every table is sorted by name, so the module diffs by
317
405
  * endpoint and never moves when registrations are reordered.
318
406
  */
319
- apiOptionEntries(): LambderApiOptionEntries;
407
+ apiOptionEntries(): Promise<LambderApiOptionEntries>;
320
408
  getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder;
321
409
  private getResolver;
322
410
  getHandler(): LambderHandler;
@@ -341,6 +429,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
341
429
  */
342
430
  private runBeforeRenderHooks;
343
431
  private handleNoMatchedAction;
432
+ /**
433
+ * What answers a request nothing matched, its beforeRender hooks run:
434
+ * the fallback hooks, then the API fallback for an API call, and
435
+ * otherwise the public files, the shell and the route fallback in turn.
436
+ */
437
+ private answerUnmatched;
344
438
  /**
345
439
  * True for the OPTIONS request the CORS layer answers by itself. Asked
346
440
  * twice: once to build the 204, once at the end of render() to pick which
@@ -446,7 +540,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
446
540
  /**
447
541
  * A thrown LambderApiRefusal (from a hook, say) as the structured API
448
542
  * envelope: the core's one mapping, after the same check the pipeline
449
- * applies, against the endpoint the call names. A name no API is
543
+ * applies, against the endpoint the call names, its lazy group loaded if
544
+ * the refusal came before the call reached it. A name no API is
450
545
  * registered under declares no code, so only an uncoded or a framework
451
546
  * refusal goes out for it.
452
547
  */
@@ -456,11 +551,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
456
551
  * The canonical way to create an instance: fix the session data type first,
457
552
  * then create with the full configuration in one declaration. The policy,
458
553
  * guard and idempotency types are inferred from the options, so the instance
459
- * is born fully typed and `typeof lambderApp` is the annotation type for api
460
- * modules. There are no ordering rules, and nothing can be half-configured.
554
+ * is born fully typed, and the endpoints declared with its defineApi are
555
+ * typed against it. There are no ordering rules, and nothing can be
556
+ * half-configured.
461
557
  *
462
558
  * ```typescript
463
- * // app.ts (imports no api modules, so modules can import the type back)
559
+ * // app.ts (imports no api modules, so modules can import from it)
464
560
  * export const lambderApp = initLambder<SessionData>().create({
465
561
  * apiPath: "/api",
466
562
  * session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: "..." },
@@ -468,13 +564,15 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
468
564
  * guards,
469
565
  * idempotency: { store },
470
566
  * });
471
- * export type AppLambder = typeof lambderApp;
567
+ * export const { defineApi, defineApiGroup, lazyApiGroup } = lambderApp;
472
568
  *
473
569
  * // orders.ts
474
- * export const orderApi = (lambder: AppLambder) => lambder.addSessionApi(...);
570
+ * export const orderApis = defineApiGroup("orders", {
571
+ * place: defineApi({ input, output, guards: "signedIn" }, async (ctx) => ...),
572
+ * });
475
573
  *
476
574
  * // index.ts: registration only
477
- * const lambder = lambderApp.addHook(...).use(orderApi)...;
575
+ * const lambder = lambderApp.registerApiGroups(orderApis).addHook(...);
478
576
  * export const handler = lambder.getHandler();
479
577
  * ```
480
578
  *
@@ -518,9 +616,18 @@ export declare const initLambder: <TSessionData = any>() => {
518
616
  * endpoint's codes alone.
519
617
  */
520
618
  refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TRefusals, keyof TRefusals & string>, TRequireCodes>;
521
- create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, TRefusals>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
522
- policies: infer TPolicies;
523
- } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {}, TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, TRefusals, TRequireCodes>;
619
+ create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, TRefusals>): Lambder<{
620
+ session: TSessionData;
621
+ policies: TOptions["rateLimits"] extends {
622
+ policies: infer TPolicies;
623
+ } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {};
624
+ guards: TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {};
625
+ idempotency: TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false;
626
+ guardsRequired: [LambderGivenOption<TOptions, "requireApiGuards">] extends [false | undefined] ? false : true;
627
+ sessions: [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true;
628
+ refusals: TRefusals;
629
+ refusalCodesRequired: TRequireCodes;
630
+ }, {}>;
524
631
  guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, TRefusals, TRequireCodes>;
525
632
  rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
526
633
  };
@@ -544,9 +651,18 @@ export declare const initLambder: <TSessionData = any>() => {
544
651
  code?: undefined;
545
652
  data?: undefined;
546
653
  }) | undefined) => never;
547
- create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireSessionApiGuards" | "requirePublicApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, {}>): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
548
- policies: infer TPolicies;
549
- } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {}, TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false, [LambderGivenOption<TOptions, "requireSessionApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "requirePublicApiGuards">] extends [false | undefined] ? false : true, [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true, {}, false>;
654
+ create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, {}>): Lambder<{
655
+ session: TSessionData;
656
+ policies: TOptions["rateLimits"] extends {
657
+ policies: infer TPolicies;
658
+ } ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {};
659
+ guards: TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {};
660
+ idempotency: TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false;
661
+ guardsRequired: [LambderGivenOption<TOptions, "requireApiGuards">] extends [false | undefined] ? false : true;
662
+ sessions: [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true;
663
+ refusals: {};
664
+ refusalCodesRequired: false;
665
+ }, {}>;
550
666
  guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, {}, false>;
551
667
  rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
552
668
  };