lambder 8.1.2 → 9.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 (86) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +23 -31
  3. package/dist/api/LambderApiCallContext.d.ts +31 -1
  4. package/dist/api/LambderApiCallContext.js +8 -0
  5. package/dist/api/LambderApiDefinition.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.d.ts +1 -1
  7. package/dist/api/LambderApiEnvelope.js +3 -4
  8. package/dist/api/LambderApiGuards.d.ts +2 -17
  9. package/dist/api/LambderApiIdempotency.js +5 -7
  10. package/dist/api/LambderApiRateLimits.d.ts +2 -29
  11. package/dist/build/generatedTables.d.ts +72 -0
  12. package/dist/build/generatedTables.js +99 -0
  13. package/dist/build/writeApiGuardParams.d.ts +60 -0
  14. package/dist/build/writeApiGuardParams.js +85 -0
  15. package/dist/build/writeApiOptions.d.ts +68 -0
  16. package/dist/build/writeApiOptions.js +102 -0
  17. package/dist/build.d.ts +10 -4
  18. package/dist/build.js +7 -4
  19. package/dist/client/LambderCaller.d.ts +0 -4
  20. package/dist/client/LambderCaller.js +1 -9
  21. package/dist/client/LambderUploadRunner.d.ts +7 -7
  22. package/dist/client/LambderUploadRunner.js +12 -21
  23. package/dist/client.d.ts +7 -0
  24. package/dist/client.js +11 -0
  25. package/dist/core/Lambder.d.ts +71 -12
  26. package/dist/core/Lambder.js +116 -38
  27. package/dist/core/LambderContext.d.ts +9 -6
  28. package/dist/core/LambderContext.js +2 -1
  29. package/dist/core/LambderResolver.d.ts +6 -12
  30. package/dist/core/LambderResolver.js +2 -14
  31. package/dist/core/LambderResponseBuilder.d.ts +15 -70
  32. package/dist/core/LambderResponseBuilder.js +15 -99
  33. package/dist/index.d.ts +16 -3
  34. package/dist/index.js +14 -1
  35. package/dist/invoke/LambderInvokeCaller.js +3 -4
  36. package/dist/mock/LambderMockApp.d.ts +34 -17
  37. package/dist/mock/LambderMockApp.js +69 -24
  38. package/dist/mock/LambderMockCreateOptions.d.ts +70 -7
  39. package/dist/mock/LambderMockTypes.d.ts +31 -21
  40. package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
  41. package/dist/mock/lambderMockPoliciesFrom.js +46 -0
  42. package/dist/mock.d.ts +3 -0
  43. package/dist/mock.js +3 -0
  44. package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
  45. package/dist/secrets/LambderOneShotSecrets.js +217 -0
  46. package/dist/session/LambderSessionCrypto.js +6 -16
  47. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
  48. package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
  49. package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
  50. package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
  51. package/dist/shared/util/LambderBackoffTimer.js +86 -0
  52. package/dist/shared/util/LambderBase64.d.ts +14 -0
  53. package/dist/shared/util/LambderBase64.js +17 -0
  54. package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
  55. package/dist/shared/util/LambderSignedClaims.js +109 -0
  56. package/dist/shared/util/LambderTextDigest.d.ts +19 -5
  57. package/dist/shared/util/LambderTextDigest.js +30 -5
  58. package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
  59. package/dist/shared/util/assertPlainData.d.ts +9 -0
  60. package/dist/shared/util/assertPlainData.js +41 -0
  61. package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
  62. package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
  63. package/dist/shared/wire/LambderApiContract.d.ts +9 -14
  64. package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
  65. package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
  66. package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
  67. package/dist/shared/wire/LambderApiRefusal.js +3 -4
  68. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
  69. package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
  70. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
  71. package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
  72. package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
  73. package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
  74. package/dist/testing/LambderConformanceRunner.d.ts +46 -0
  75. package/dist/testing/LambderConformanceRunner.js +21 -0
  76. package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
  77. package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
  78. package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
  79. package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
  80. package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
  81. package/dist/testing/lambderRateLimiterConformance.js +72 -0
  82. package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
  83. package/dist/testing/lambderSessionStoreConformance.js +165 -0
  84. package/dist/testing.d.ts +14 -0
  85. package/dist/testing.js +12 -0
  86. package/package.json +1 -1
package/dist/client.d.ts CHANGED
@@ -26,6 +26,8 @@ export type { LambderIdempotencyKeyScope } from "./shared/wire/LambderIdempotenc
26
26
  export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
27
27
  export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderAppRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
28
28
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
29
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
30
+ export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
29
31
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
30
32
  export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
31
33
  export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/wire/LambderRequestPayload.js";
@@ -36,6 +38,11 @@ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtm
36
38
  export { createLambderI18n } from "./shared/LambderI18n.js";
37
39
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
38
40
  export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
41
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
42
+ export type { LambderBackoffTimerOptions } from "./shared/util/LambderBackoffTimer.js";
43
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
44
+ export type { LambderSignedClaimsOptions } from "./shared/util/LambderSignedClaims.js";
45
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
39
46
  export { LambderUploadRunner, LambderUploadError } from "./client/LambderUploadRunner.js";
40
47
  export type { LambderUploadRunnerOptions, LambderUploadProgress, LambderUploadPhase, LambderUploadFailureReason } from "./client/LambderUploadRunner.js";
41
48
  export { checkUploadRule } from "./shared/contracts/LambderUploadBucket.js";
package/dist/client.js CHANGED
@@ -23,6 +23,9 @@ export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/L
23
23
  // Typed API refusals (isomorphic: shared code may throw them from anywhere;
24
24
  // in the browser they are plain Errors).
25
25
  export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
26
+ // The server's declared options as plain data (the generated options module's
27
+ // entry types) and the readers a client derives its own facts from.
28
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
26
29
  // A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
27
30
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
28
31
  // Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
@@ -33,6 +36,14 @@ export { resolveCompressionOption } from "./shared/wire/LambderCompressionOption
33
36
  export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml } from "./shared/LambderHtml.js";
34
37
  // Typed translations (standalone, isomorphic)
35
38
  export { createLambderI18n } from "./shared/LambderI18n.js";
39
+ // Waiting longer after each failure, once: for a reconnecting client or a
40
+ // screen that has to come back by itself.
41
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
42
+ // Signed claims tokens, for the isomorphic code that verifies them where a
43
+ // server's secret is at hand (an edge Worker, a shared backend package);
44
+ // never in a page, which holds no secret to verify with.
45
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
46
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
36
47
  // Direct uploads: the runner that takes a file from the browser straight to
37
48
  // storage, and the vocabulary it shares with the server's bucket.
38
49
  export { LambderUploadRunner, LambderUploadError } from "./client/LambderUploadRunner.js";
@@ -14,13 +14,14 @@ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js
14
14
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
15
15
  import { type LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
16
16
  import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
17
+ import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
17
18
  import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
18
19
  import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf } from "../api/LambderApiGuards.js";
19
20
  import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
20
21
  import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
21
22
  import type { LambderContractEntry, LambderJsonOutputOf, LambderMergeContract } from "../shared/wire/LambderApiContract.js";
22
23
  import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
23
- import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
24
+ import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
24
25
  import { type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderNoExtraKeys, type LambderRequirableGuardsField, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
25
26
  /** Everything `lambder/testing` may put under a built instance: the pipeline's stores, and the source its files are read from. */
26
27
  export type LambderInstanceBackends = LambderPipelineBackends & {
@@ -91,6 +92,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
91
92
  private readonly apiDefinitions;
92
93
  /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
93
94
  private readonly guards;
95
+ /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
96
+ private readonly rateLimitPolicies;
94
97
  private hookList;
95
98
  private createdHooks;
96
99
  private initPromise;
@@ -152,7 +155,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
152
155
  addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
153
156
  addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TSessionData, LambderPathParamsOf<TPath>, {}, _TRateLimitPolicies>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
154
157
  addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TSessionData> & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
155
- addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, false> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never>(name: TName, schema: {
158
+ addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, false> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never>(name: TName, schema: {
156
159
  input: TInput;
157
160
  output: TOutput;
158
161
  } & {
@@ -160,8 +163,18 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
160
163
  rateLimit?: TRateOpt;
161
164
  /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
162
165
  idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
163
- } & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt>, handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, TSessionData, _TRateLimitPolicies>, resolver: LambderResolver<z.input<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOutputOf<z.output<TOutput>>, "public", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled>;
164
- addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never>(name: TName, schema: {
166
+ /**
167
+ * Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
168
+ * body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
169
+ * compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
170
+ * cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
171
+ * both ends. A transport setting of this server's, not part of the API's contract.
172
+ */
173
+ compress?: boolean | "auto";
174
+ } & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt>,
175
+ /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
176
+ handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, TSessionData, _TRateLimitPolicies>) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOutputOf<z.output<TOutput>>, "public", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled>;
177
+ addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.input<TInput>, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.input<TInput>, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never>(name: TName, schema: {
165
178
  input: TInput;
166
179
  output: TOutput;
167
180
  } & {
@@ -169,7 +182,17 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
169
182
  rateLimit?: TRateOpt;
170
183
  /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
171
184
  idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
172
- } & LambderRequirableGuardsField<_TSessionGuardsRequired, TGuardsOpt> & LambderSessionEnabledInstance<_TSessionsEnabled>, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, _TRateLimitPolicies>, resolver: LambderResolver<z.input<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOutputOf<z.output<TOutput>>, "session", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled>;
185
+ /**
186
+ * Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
187
+ * body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
188
+ * compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
189
+ * cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
190
+ * both ends. A transport setting of this server's, not part of the API's contract.
191
+ */
192
+ compress?: boolean | "auto";
193
+ } & LambderRequirableGuardsField<_TSessionGuardsRequired, TGuardsOpt> & LambderSessionEnabledInstance<_TSessionsEnabled>,
194
+ /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
195
+ handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, _TRateLimitPolicies>) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOutputOf<z.output<TOutput>>, "session", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled>;
173
196
  /**
174
197
  * What registering an API is, for addApi and addSessionApi alike: the
175
198
  * checks that can refuse it, then its definition recorded (what
@@ -246,7 +269,25 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
246
269
  * unless the generator writes it there.
247
270
  */
248
271
  apiSignatureEntries(): Promise<LambderApiSignatureEntry[]>;
249
- getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder<any>;
272
+ /**
273
+ * Every registered API's mode and declared options as plain data, with
274
+ * the rate-limit policies and guards they name reduced to what is not
275
+ * code: what writeApiOptions (lambder/build) writes to a module a client,
276
+ * a mock or a test imports instead of the server. The contract carries
277
+ * the same options as types; this is the same fact as a value, for code
278
+ * that decides something at runtime with it.
279
+ *
280
+ * Nothing here is a secret or a handler by construction. A guard's
281
+ * parameter is written as it was declared, so it has to be plain data
282
+ * (a permission string, a list, a reason); one that is not fails by API
283
+ * and guard name. A policy's key handler is never written: its `per`
284
+ * says "custom" and no more. A guard's input schema is never written
285
+ * either; its declaration says only which of the three input modes it
286
+ * has. Every table is sorted by name, so the module diffs by endpoint
287
+ * and never moves when registrations are reordered.
288
+ */
289
+ apiOptionEntries(): LambderApiOptionEntries;
290
+ getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder;
250
291
  private getResolver;
251
292
  getHandler(): LambderHandler;
252
293
  /** True when the Lambda event is an API Gateway HTTP event (REST API v1 or HTTP API / Function URL v2). */
@@ -344,12 +385,30 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
344
385
  private inputValidationRefusal;
345
386
  /**
346
387
  * One API call through the core: the pipeline runs the protocol steps and
347
- * calls back for the handler, whose LambderResponse (returned, or thrown
348
- * via res.die.*) becomes the answer the pipeline stores and hands back.
349
- * The context is the pipeline's context, so a session it fetched is on
350
- * ctx.session and the validated payload is on ctx.apiPayload when the
351
- * handler runs. The handler's resolver knows the API's output schema, so
352
- * every success payload is parsed through it before it is sent.
388
+ * calls back for the handler, whose returned output becomes the answer
389
+ * the pipeline stores and hands back. The context is the pipeline's
390
+ * context, so a session it fetched is on ctx.session and the validated
391
+ * payload is on ctx.apiPayload when the handler runs.
392
+ *
393
+ * The output goes out as the API's schema declares it. The type system
394
+ * accepts a value that carries more than the schema (a row read straight
395
+ * from a table is assignable to a narrower object type), and without the
396
+ * parse the extra fields, a password hash included, would reach the
397
+ * client. zod strips what the schema does not declare, fills its defaults
398
+ * and applies its transforms, so the wire and the idempotency store only
399
+ * see the declared shape. The handler returns the schema's input form, so
400
+ * a transform runs exactly once.
401
+ *
402
+ * An output the schema rejects is the handler breaking its contract,
403
+ * answered as a crash rather than sent (LambderApiOutputValidationError,
404
+ * which an idempotency key records as its answer, since the handler has
405
+ * already run). The parse is synchronous, so an output schema cannot be
406
+ * async: zod throws from a synchronous parse that meets an async
407
+ * refinement or transform, and a transform may throw of its own accord.
408
+ * Either throw becomes the same error, carrying what was thrown as its
409
+ * cause. Left to escape as it is, it would read as the handler crashing
410
+ * before its answer: the idempotency engine would release the key's claim
411
+ * and every retry would run the operation again.
353
412
  */
354
413
  private runApi;
355
414
  /** A thrown LambderApiRefusal (from a hook, say) as the structured API envelope: the core's one mapping. */
@@ -15,7 +15,9 @@ import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
15
15
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
16
16
  import { apiSignatureOf } from "../api/LambderApiSignature.js";
17
17
  import { apiNameKeyOf } from "../shared/wire/LambderApiSignature.js";
18
- import { apiNotFoundAnswer, refusalAnswer, sessionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
18
+ import { assertPlainData } from "../shared/util/assertPlainData.js";
19
+ import { apiNotFoundAnswer, buildApiEnvelope, envelopeAnswer, refusalAnswer, sessionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
20
+ import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
19
21
  import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
20
22
  import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
21
23
  import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
@@ -80,6 +82,8 @@ export default class Lambder {
80
82
  apiDefinitions = new Map();
81
83
  /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
82
84
  guards;
85
+ /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
86
+ rateLimitPolicies;
83
87
  hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
84
88
  createdHooks = [];
85
89
  initPromise = null;
@@ -122,6 +126,7 @@ export default class Lambder {
122
126
  }
123
127
  const session = options.session;
124
128
  this.guards = options.guards;
129
+ this.rateLimitPolicies = options.rateLimits?.policies;
125
130
  this.pipeline = new LambderApiPipeline({
126
131
  apiVersion: this.apiVersion,
127
132
  minApiVersion: options.minApiVersion,
@@ -259,12 +264,16 @@ export default class Lambder {
259
264
  return this;
260
265
  }
261
266
  // Typed API with Zod
262
- addApi(name, schema, handler) {
267
+ addApi(name, schema,
268
+ /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
269
+ handler) {
263
270
  this.registerApi(name, "public", schema, handler);
264
271
  return this;
265
272
  }
266
273
  // Typed Session API with Zod
267
- addSessionApi(name, schema, handler) {
274
+ addSessionApi(name, schema,
275
+ /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
276
+ handler) {
268
277
  this.registerApi(name, "session", schema, handler);
269
278
  return this;
270
279
  }
@@ -299,7 +308,7 @@ export default class Lambder {
299
308
  this.apiDefinitions.set(name, definition);
300
309
  this.actionList.push({
301
310
  match: (ctx) => ctx.apiName === name ? {} : false,
302
- actionFn: (ctx) => this.runApi(ctx, definition, handler),
311
+ actionFn: (ctx) => this.runApi(ctx, definition, schema.output, schema.compress ?? "auto", handler),
303
312
  });
304
313
  }
305
314
  addHook(hookEvent, hookFn, priority = 0) {
@@ -413,6 +422,71 @@ export default class Lambder {
413
422
  entries.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
414
423
  return entries;
415
424
  }
425
+ /**
426
+ * Every registered API's mode and declared options as plain data, with
427
+ * the rate-limit policies and guards they name reduced to what is not
428
+ * code: what writeApiOptions (lambder/build) writes to a module a client,
429
+ * a mock or a test imports instead of the server. The contract carries
430
+ * the same options as types; this is the same fact as a value, for code
431
+ * that decides something at runtime with it.
432
+ *
433
+ * Nothing here is a secret or a handler by construction. A guard's
434
+ * parameter is written as it was declared, so it has to be plain data
435
+ * (a permission string, a list, a reason); one that is not fails by API
436
+ * and guard name. A policy's key handler is never written: its `per`
437
+ * says "custom" and no more. A guard's input schema is never written
438
+ * either; its declaration says only which of the three input modes it
439
+ * has. Every table is sorted by name, so the module diffs by endpoint
440
+ * and never moves when registrations are reordered.
441
+ */
442
+ apiOptionEntries() {
443
+ const apis = {};
444
+ for (const name of [...this.apiDefinitions.keys()].sort()) {
445
+ const { mode, guards, rateLimit, idempotency } = this.apiDefinitions.get(name);
446
+ const entry = { mode };
447
+ if (guards !== undefined) {
448
+ assertPlainData(guards, `the guards option of API "${name}"`);
449
+ entry.guards = guards;
450
+ }
451
+ if (rateLimit !== undefined) {
452
+ assertPlainData(rateLimit, `the rateLimit option of API "${name}"`);
453
+ entry.rateLimit = rateLimit;
454
+ }
455
+ if (idempotency !== undefined)
456
+ entry.idempotency = idempotency;
457
+ apis[name] = entry;
458
+ }
459
+ const rateLimitPolicies = {};
460
+ for (const name of Object.keys(this.rateLimitPolicies ?? {}).sort()) {
461
+ const { per, budget, chargeAt, errorMessage, ...windows } = this.rateLimitPolicies[name];
462
+ const entry = {};
463
+ for (const [window, limit] of Object.entries(windows)) {
464
+ if (limit !== undefined)
465
+ entry[window] = limit;
466
+ }
467
+ if (per !== undefined)
468
+ entry.per = per === "ip" || per === "session" ? per : "custom";
469
+ if (budget !== undefined)
470
+ entry.budget = budget;
471
+ if (chargeAt !== undefined)
472
+ entry.chargeAt = chargeAt;
473
+ if (errorMessage !== undefined) {
474
+ assertPlainData(errorMessage, `the errorMessage of rate-limit policy "${name}"`);
475
+ entry.errorMessage = errorMessage;
476
+ }
477
+ rateLimitPolicies[name] = entry;
478
+ }
479
+ const guards = {};
480
+ for (const name of Object.keys(this.guards ?? {}).sort()) {
481
+ const guard = this.guards[name];
482
+ guards[name] = {
483
+ input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
484
+ session: guard.session === true,
485
+ runAt: guard.runAt ?? "beforeInputValidation",
486
+ };
487
+ }
488
+ return { apis, rateLimitPolicies, guards };
489
+ }
416
490
  getResponseBuilder(ctx) {
417
491
  return new LambderResponseBuilder({
418
492
  files: this.files,
@@ -631,8 +705,8 @@ export default class Lambder {
631
705
  catch (err) {
632
706
  response = (await this.answerThrown(err, ctx, resolver)).copy();
633
707
  }
634
- // Only what the hooks themselves wrote (res.setHeader inside a
635
- // hook) is left to apply, which leaves their overrides standing.
708
+ // Only what the hooks themselves wrote (ctx.setResponseHeader
709
+ // inside a hook) is left to apply, which leaves their overrides standing.
636
710
  // A hook that answered with a different response takes the whole
637
711
  // set instead: headers belong to the call, not to the response
638
712
  // that first carried them, so the call's session cookie must
@@ -814,48 +888,52 @@ export default class Lambder {
814
888
  }
815
889
  /**
816
890
  * One API call through the core: the pipeline runs the protocol steps and
817
- * calls back for the handler, whose LambderResponse (returned, or thrown
818
- * via res.die.*) becomes the answer the pipeline stores and hands back.
819
- * The context is the pipeline's context, so a session it fetched is on
820
- * ctx.session and the validated payload is on ctx.apiPayload when the
821
- * handler runs. The handler's resolver knows the API's output schema, so
822
- * every success payload is parsed through it before it is sent.
891
+ * calls back for the handler, whose returned output becomes the answer
892
+ * the pipeline stores and hands back. The context is the pipeline's
893
+ * context, so a session it fetched is on ctx.session and the validated
894
+ * payload is on ctx.apiPayload when the handler runs.
895
+ *
896
+ * The output goes out as the API's schema declares it. The type system
897
+ * accepts a value that carries more than the schema (a row read straight
898
+ * from a table is assignable to a narrower object type), and without the
899
+ * parse the extra fields, a password hash included, would reach the
900
+ * client. zod strips what the schema does not declare, fills its defaults
901
+ * and applies its transforms, so the wire and the idempotency store only
902
+ * see the declared shape. The handler returns the schema's input form, so
903
+ * a transform runs exactly once.
904
+ *
905
+ * An output the schema rejects is the handler breaking its contract,
906
+ * answered as a crash rather than sent (LambderApiOutputValidationError,
907
+ * which an idempotency key records as its answer, since the handler has
908
+ * already run). The parse is synchronous, so an output schema cannot be
909
+ * async: zod throws from a synchronous parse that meets an async
910
+ * refinement or transform, and a transform may throw of its own accord.
911
+ * Either throw becomes the same error, carrying what was thrown as its
912
+ * cause. Left to escape as it is, it would read as the handler crashing
913
+ * before its answer: the idempotency engine would release the key's claim
914
+ * and every retry would run the operation again.
823
915
  */
824
- async runApi(ctx, definition, handler) {
916
+ async runApi(ctx, definition, output, compress, handler) {
825
917
  const request = ctx.api;
826
918
  if (!request)
827
919
  throw new Error(`Lambder: API "${definition.name}" was matched by a request that is not an API call.`);
828
- const resolver = new LambderResolver({ files: this.files, apiVersion: this.apiVersion, ctx, apiOutput: definition.output });
829
- // What the handler produced, in both forms: the answer went to the
830
- // pipeline, and the response is kept so it can carry on unchanged.
831
- const handled = { output: null };
832
920
  const { answer } = await this.pipeline.run(request, ctx, definition, async () => {
833
921
  ctx.apiPayload = request.payload;
834
- let response;
922
+ const returned = await handler(ctx);
923
+ let parsed;
835
924
  try {
836
- response = await handler(ctx, resolver);
925
+ parsed = output.safeParse(returned);
837
926
  }
838
- catch (err) {
839
- // A thrown LambderResponse IS the response (res.die.*): an
840
- // answer like a returned one, stored and replayed alike.
841
- if (err instanceof LambderResponse)
842
- response = err;
843
- else
844
- throw err;
927
+ catch (thrown) {
928
+ throw new LambderApiOutputValidationError(definition.name, { thrown });
845
929
  }
846
- handled.output = { response, answer: answerFromResponse(response) };
847
- return handled.output.answer;
930
+ if (!parsed.success)
931
+ throw new LambderApiOutputValidationError(definition.name, { zodError: parsed.error });
932
+ return envelopeAnswer(buildApiEnvelope(this.apiVersion, parsed.data, { logList: ctx.logList }));
848
933
  });
849
- // When the pipeline answered with the handler's own answer, its
850
- // response carries on rather than a rebuild. An answer holds a Buffer
851
- // body base64-encoded (the plain shape the idempotency store
852
- // persists), and a response rebuilt from it would hand finalization a
853
- // base64 string it must pass through uncompressed. Identity decides,
854
- // since the pipeline may have answered with a stored replay or a
855
- // refusal instead.
856
- if (handled.output?.answer === answer)
857
- return handled.output.response;
858
- return responseFromAnswer(answer);
934
+ // The API's compress option, on whatever answer the call ended with:
935
+ // a replayed one comes back from its store without the hint.
936
+ return responseFromAnswer({ ...answer, compress });
859
937
  }
860
938
  /** A thrown LambderApiRefusal (from a hook, say) as the structured API envelope: the core's one mapping. */
861
939
  apiErrorResponse(err, ctx) {
@@ -2,6 +2,7 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyEvent
2
2
  import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
3
3
  import { type LambderApiRequest } from "../api/LambderApiRequest.js";
4
4
  import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
5
+ import { type LambderResponseTools } from "../api/LambderApiCallContext.js";
5
6
  import type LambderSessionController from "../session/LambderSessionController.js";
6
7
  import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult } from "../api/LambderApiRateLimits.js";
7
8
  export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
@@ -19,7 +20,9 @@ export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxy
19
20
  * API core's call context (session, guardData, responseHeaders, logList),
20
21
  * which is the part the pipeline and the session controller work on; the
21
22
  * rest is the HTTP request as the Lambda event delivered it, plus the tools
22
- * the instance rendering it binds on (sessionController, rateLimit, isRateLimited).
23
+ * the instance rendering it binds on (sessionController, rateLimit, isRateLimited)
24
+ * and the response tools (setResponseHeader, addResponseHeader, setCookie,
25
+ * clearCookie) that write onto whatever answer the request ends with.
23
26
  *
24
27
  * TRateLimitPolicies is the app's policies map on a handler registered with
25
28
  * addApi, addSessionApi, addRoute or addSessionRoute, so a policy name is
@@ -102,9 +105,9 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
102
105
  lambdaContext: Context;
103
106
  /** Which API Gateway payload format the event arrived in, and the response leaves in. */
104
107
  eventFormat: LambderHttpEventFormat;
105
- /** Response headers written during the request (res.setHeader, res.addHeader, session cookies), applied onto the response at the end. */
108
+ /** Response headers written during the request (the response tools below, session cookies), applied onto the response at the end. */
106
109
  responseHeaders: LambderAnswerHeaders;
107
- /** Entries for the API envelope's logList channel (res.logToApiResponse). */
110
+ /** Entries for the API envelope's logList channel: a handler pushes what it wants the caller's debug log to show. */
108
111
  logList: unknown[];
109
112
  /**
110
113
  * Sessions for this request: read the one it carries
@@ -125,12 +128,12 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
125
128
  rateLimit: LambderContextRateLimit<TRateLimitPolicies>;
126
129
  /** The same count as rateLimit, answered instead of thrown: false, or the window that refused and its retryAfterSeconds. */
127
130
  isRateLimited: LambderContextRateLimitCheck<TRateLimitPolicies>;
128
- };
131
+ } & LambderResponseTools;
129
132
  export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData, SessionData, TRateLimitPolicies>, 'session'> & {
130
133
  session: LambderSessionRecord<SessionData>;
131
134
  };
132
- /** The members of a render context that belong to the instance rendering the request rather than to its event. */
133
- type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited";
135
+ /** The members of a render context that are bound onto it rather than read from its event. */
136
+ type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited" | keyof LambderResponseTools;
134
137
  /** What an instance binds onto each context it renders: see bindContextTools. */
135
138
  export type LambderContextTools = {
136
139
  sessionControllerFor: (ctx: LambderRenderContext) => LambderSessionController<any>;
@@ -4,7 +4,7 @@ import { base64ToText } from "../shared/util/LambderBase64.js";
4
4
  import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
5
5
  import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
6
6
  import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
7
- import { bindCallTools } from "../api/LambderApiCallContext.js";
7
+ import { bindCallTools, responseToolsOf } from "../api/LambderApiCallContext.js";
8
8
  import { decodeRequestPath } from "./LambderRequestPath.js";
9
9
  /** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
10
10
  export const isV2HttpEvent = (event) => !!event && typeof event === "object"
@@ -23,6 +23,7 @@ export const bindContextTools = (ctx, tools) => {
23
23
  methods: {
24
24
  rateLimit: async (policy, key) => { await tools.chargeRateLimit(bound, policy, key, true); },
25
25
  isRateLimited: (policy, key) => tools.chargeRateLimit(bound, policy, key, false),
26
+ ...responseToolsOf(bound, bound.host),
26
27
  },
27
28
  });
28
29
  return bound;
@@ -1,10 +1,9 @@
1
- import type { LambderApiNullAnswerConfig } from "../shared/wire/LambderApiContract.js";
2
- import LambderResponseBuilder, { type LambderResolverApiMethod, type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
1
+ import LambderResponseBuilder from "./LambderResponseBuilder.js";
3
2
  import type { LambderResponse } from "./LambderResponse.js";
4
3
  type SyncDie<T extends (...args: any[]) => LambderResponse> = (...args: Parameters<T>) => never;
5
4
  type AsyncDie<T extends (...args: any[]) => Promise<LambderResponse>> = (...args: Parameters<T>) => Promise<never>;
6
5
  /** The `res.die.*` surface: every builder method, throwing what it built. Internal to the resolver, which is the only thing that has one. */
7
- interface DieResolverMethods<TOutput> {
6
+ interface DieResolverMethods {
8
7
  raw: SyncDie<LambderResponseBuilder["raw"]>;
9
8
  json: SyncDie<LambderResponseBuilder["json"]>;
10
9
  text: SyncDie<LambderResponseBuilder["text"]>;
@@ -15,25 +14,20 @@ interface DieResolverMethods<TOutput> {
15
14
  redirect: SyncDie<LambderResponseBuilder["redirect"]>;
16
15
  versionExpired: SyncDie<LambderResponseBuilder["versionExpired"]>;
17
16
  fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
18
- api: LambderResolverApiMethod<TOutput, never>;
19
- apiBinary: LambderResolverApiMethod<TOutput, never>;
17
+ api: SyncDie<LambderResponseBuilder["api"]>;
20
18
  file: AsyncDie<LambderResponseBuilder["file"]>;
21
19
  templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
22
20
  }
23
21
  /**
24
- * Response builder passed to route/api handlers and hooks.
22
+ * Response builder passed to route handlers and hooks.
25
23
  *
26
24
  * `res.die.*` builds the response and THROWS it, immediately halting the
27
25
  * request at any call depth (handlers, hooks, nested service functions).
28
26
  * Lambder's render pipeline catches thrown LambderResponse instances and uses
29
27
  * them as the response. Plain `throw res.html(...)` works the same way.
30
28
  */
31
- export default class LambderResolver<TOutput = any> extends LambderResponseBuilder<TOutput> {
32
- die: DieResolverMethods<TOutput>;
29
+ export default class LambderResolver extends LambderResponseBuilder {
30
+ die: DieResolverMethods;
33
31
  constructor(...args: ConstructorParameters<typeof LambderResponseBuilder>);
34
- api(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
35
- api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
36
- apiBinary(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
37
- apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
38
32
  }
39
33
  export {};
@@ -1,6 +1,6 @@
1
1
  import LambderResponseBuilder from "./LambderResponseBuilder.js";
2
2
  /**
3
- * Response builder passed to route/api handlers and hooks.
3
+ * Response builder passed to route handlers and hooks.
4
4
  *
5
5
  * `res.die.*` builds the response and THROWS it, immediately halting the
6
6
  * request at any call depth (handlers, hooks, nested service functions).
@@ -22,21 +22,9 @@ export default class LambderResolver extends LambderResponseBuilder {
22
22
  redirect: (...a) => { throw this.redirect(...a); },
23
23
  versionExpired: (...a) => { throw this.versionExpired(...a); },
24
24
  fileBase64: (...a) => { throw this.fileBase64(...a); },
25
- // Overloaded on the payload (see LambderApiAnswer); the implementation takes both shapes.
26
- api: ((payload, config, options) => {
27
- throw this.api(payload, config, options);
28
- }),
29
- apiBinary: ((payload, config, options) => {
30
- throw this.apiBinary(payload, config, options);
31
- }),
25
+ api: (...a) => { throw this.api(...a); },
32
26
  file: async (...a) => { throw await this.file(...a); },
33
27
  templateFile: async (...a) => { throw await this.templateFile(...a); },
34
28
  };
35
29
  }
36
- api(payload, config, options) {
37
- return super.api(payload, config, options);
38
- }
39
- apiBinary(payload, config, options) {
40
- return super.apiBinary(payload, config, options);
41
- }
42
30
  }