lambder 8.3.1 → 9.0.2

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 (41) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +18 -25
  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/LambderApiIdempotency.js +5 -7
  9. package/dist/api/LambderApiPipeline.d.ts +1 -1
  10. package/dist/api/LambderApiPipeline.js +1 -1
  11. package/dist/api/LambderApiSignature.js +1 -1
  12. package/dist/client/LambderCaller.d.ts +1 -5
  13. package/dist/client/LambderCaller.js +2 -10
  14. package/dist/client.d.ts +2 -2
  15. package/dist/client.js +1 -1
  16. package/dist/core/Lambder.d.ts +51 -13
  17. package/dist/core/Lambder.js +48 -39
  18. package/dist/core/LambderContext.d.ts +9 -6
  19. package/dist/core/LambderContext.js +2 -1
  20. package/dist/core/LambderCreateOptions.d.ts +1 -1
  21. package/dist/core/LambderResolver.d.ts +6 -12
  22. package/dist/core/LambderResolver.js +2 -14
  23. package/dist/core/LambderResponseBuilder.d.ts +15 -70
  24. package/dist/core/LambderResponseBuilder.js +15 -99
  25. package/dist/index.d.ts +5 -5
  26. package/dist/index.js +2 -2
  27. package/dist/invoke/LambderInvokeCaller.d.ts +1 -1
  28. package/dist/invoke/LambderInvokeCaller.js +4 -5
  29. package/dist/mock/LambderMockApp.js +2 -3
  30. package/dist/mock/LambderMockCreateOptions.d.ts +3 -3
  31. package/dist/mock/LambderMockTypes.d.ts +2 -11
  32. package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
  33. package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
  34. package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
  35. package/dist/shared/wire/LambderApiContract.d.ts +9 -14
  36. package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
  37. package/dist/shared/wire/LambderApiRefusal.js +3 -4
  38. package/dist/testing/LambderTestVisitor.d.ts +1 -1
  39. package/package.json +1 -1
  40. /package/dist/shared/wire/{LambderApiSignature.d.ts → LambderApiSignatureMap.d.ts} +0 -0
  41. /package/dist/shared/wire/{LambderApiSignature.js → LambderApiSignatureMap.js} +0 -0
@@ -14,9 +14,10 @@ import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
14
14
  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
- import { apiNameKeyOf } from "../shared/wire/LambderApiSignature.js";
17
+ import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
18
18
  import { assertPlainData } from "../shared/util/assertPlainData.js";
19
- import { apiNotFoundAnswer, refusalAnswer, sessionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
19
+ import { apiNotFoundAnswer, buildApiEnvelope, envelopeAnswer, refusalAnswer, sessionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
20
+ import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
20
21
  import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
21
22
  import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
22
23
  import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
@@ -263,12 +264,16 @@ export default class Lambder {
263
264
  return this;
264
265
  }
265
266
  // Typed API with Zod
266
- 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) {
267
270
  this.registerApi(name, "public", schema, handler);
268
271
  return this;
269
272
  }
270
273
  // Typed Session API with Zod
271
- 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) {
272
277
  this.registerApi(name, "session", schema, handler);
273
278
  return this;
274
279
  }
@@ -303,7 +308,7 @@ export default class Lambder {
303
308
  this.apiDefinitions.set(name, definition);
304
309
  this.actionList.push({
305
310
  match: (ctx) => ctx.apiName === name ? {} : false,
306
- actionFn: (ctx) => this.runApi(ctx, definition, handler),
311
+ actionFn: (ctx) => this.runApi(ctx, definition, schema.output, schema.compress ?? "auto", handler),
307
312
  });
308
313
  }
309
314
  addHook(hookEvent, hookFn, priority = 0) {
@@ -700,8 +705,8 @@ export default class Lambder {
700
705
  catch (err) {
701
706
  response = (await this.answerThrown(err, ctx, resolver)).copy();
702
707
  }
703
- // Only what the hooks themselves wrote (res.setHeader inside a
704
- // 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.
705
710
  // A hook that answered with a different response takes the whole
706
711
  // set instead: headers belong to the call, not to the response
707
712
  // that first carried them, so the call's session cookie must
@@ -883,48 +888,52 @@ export default class Lambder {
883
888
  }
884
889
  /**
885
890
  * One API call through the core: the pipeline runs the protocol steps and
886
- * calls back for the handler, whose LambderResponse (returned, or thrown
887
- * via res.die.*) becomes the answer the pipeline stores and hands back.
888
- * The context is the pipeline's context, so a session it fetched is on
889
- * ctx.session and the validated payload is on ctx.apiPayload when the
890
- * handler runs. The handler's resolver knows the API's output schema, so
891
- * 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.
892
915
  */
893
- async runApi(ctx, definition, handler) {
916
+ async runApi(ctx, definition, output, compress, handler) {
894
917
  const request = ctx.api;
895
918
  if (!request)
896
919
  throw new Error(`Lambder: API "${definition.name}" was matched by a request that is not an API call.`);
897
- const resolver = new LambderResolver({ files: this.files, apiVersion: this.apiVersion, ctx, apiOutput: definition.output });
898
- // What the handler produced, in both forms: the answer went to the
899
- // pipeline, and the response is kept so it can carry on unchanged.
900
- const handled = { output: null };
901
920
  const { answer } = await this.pipeline.run(request, ctx, definition, async () => {
902
921
  ctx.apiPayload = request.payload;
903
- let response;
922
+ const returned = await handler(ctx);
923
+ let parsed;
904
924
  try {
905
- response = await handler(ctx, resolver);
925
+ parsed = output.safeParse(returned);
906
926
  }
907
- catch (err) {
908
- // A thrown LambderResponse IS the response (res.die.*): an
909
- // answer like a returned one, stored and replayed alike.
910
- if (err instanceof LambderResponse)
911
- response = err;
912
- else
913
- throw err;
927
+ catch (thrown) {
928
+ throw new LambderApiOutputValidationError(definition.name, { thrown });
914
929
  }
915
- handled.output = { response, answer: answerFromResponse(response) };
916
- 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 }));
917
933
  });
918
- // When the pipeline answered with the handler's own answer, its
919
- // response carries on rather than a rebuild. An answer holds a Buffer
920
- // body base64-encoded (the plain shape the idempotency store
921
- // persists), and a response rebuilt from it would hand finalization a
922
- // base64 string it must pass through uncompressed. Identity decides,
923
- // since the pipeline may have answered with a stored replay or a
924
- // refusal instead.
925
- if (handled.output?.answer === answer)
926
- return handled.output.response;
927
- 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 });
928
937
  }
929
938
  /** A thrown LambderApiRefusal (from a hook, say) as the structured API envelope: the core's one mapping. */
930
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,5 +1,5 @@
1
1
  import type { z } from "zod";
2
- import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
2
+ import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
3
3
  import type { Context } from "aws-lambda";
4
4
  import type LambderResolver from "./LambderResolver.js";
5
5
  import type LambderResponseBuilder from "./LambderResponseBuilder.js";
@@ -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
  }
@@ -1,12 +1,10 @@
1
- import type { z } from "zod";
2
1
  import type { LambderRenderContext } from "./LambderContext.js";
3
- import { type LambderCookieOptions, type LambderClearCookieOptions } from "../shared/wire/LambderCookie.js";
4
2
  import type { LambderFiles } from "./LambderFiles.js";
5
3
  import { LambderResponse, type LambderHeadersInput } from "./LambderResponse.js";
6
4
  import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
7
5
  import { LambderSafeHtml } from "../shared/LambderHtml.js";
8
6
  import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
9
- import type { LambderApiResponseConfig, LambderApiNullAnswerConfig } from "../shared/wire/LambderApiContract.js";
7
+ import type { LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
10
8
  export type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
11
9
  export type LambderResponseOptions = {
12
10
  statusCode?: LambderHttpStatusCode;
@@ -18,19 +16,6 @@ export type LambderResponseOptions = {
18
16
  /** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
19
17
  etag?: boolean | "auto";
20
18
  };
21
- /**
22
- * The two shapes of an API answer: the output the contract declares, or
23
- * `null` beside a config that says why (a refusal flag, an `errorMessage`, a
24
- * `message`). A bare `res.api(null)` compiles only when the output type
25
- * itself allows null, so a success payload is always the declared output,
26
- * which lets a typed caller (LambderInvokeCaller.api) promise it. Untyped
27
- * resolvers (`TOutput = any`) accept anything. This is the resolver's method
28
- * type; the core's answer type is LambderApiAnswer.
29
- */
30
- export type LambderResolverApiMethod<TOutput, TResult> = {
31
- (payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): TResult;
32
- (payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): TResult;
33
- };
34
19
  export type LambderRawResponseInit = {
35
20
  statusCode: LambderHttpStatusCode;
36
21
  headers?: LambderHeadersInput;
@@ -40,39 +25,23 @@ export type LambderRawResponseInit = {
40
25
  compress?: boolean | "auto";
41
26
  etag?: boolean | "auto";
42
27
  };
43
- export default class LambderResponseBuilder<TResponse = any> {
28
+ /**
29
+ * Builds the responses of routes, hooks and error handlers. An API handler
30
+ * never holds one: it returns its output or refuses, and writes headers and
31
+ * cookies through its context (LambderResponseTools).
32
+ */
33
+ export default class LambderResponseBuilder {
44
34
  protected files: LambderFiles | null;
45
35
  protected apiVersion: string | null;
46
36
  protected ctx?: LambderRenderContext;
47
- /** The output schema of the API this builder answers, which every success payload is parsed through; null outside an API handler. */
48
- protected apiOutput: z.ZodType | null;
49
- constructor({ files, apiVersion, ctx, apiOutput }: {
37
+ constructor({ files, apiVersion, ctx }: {
50
38
  files?: LambderFiles | null;
51
39
  apiVersion?: string | null;
52
40
  ctx?: LambderRenderContext;
53
- apiOutput?: z.ZodType;
54
41
  });
55
42
  private buildResponse;
56
43
  /** The instance's file reader, which res.file and res.templateFile need. */
57
44
  private requireFiles;
58
- /** Appends a response header; applied onto the response once the handler has one, in call order. */
59
- addHeader(key: string, value: string): void;
60
- /** Replaces a response header; applied onto the response once the handler has one, in call order. */
61
- setHeader(key: string, value: string | string[]): void;
62
- /**
63
- * Adds a Set-Cookie header. A function-form `domain` is resolved against
64
- * the request hostname. Defaults: Path=/, SameSite=Lax, Secure, not
65
- * HttpOnly, browser-session lifetime.
66
- */
67
- setCookie(name: string, value: string, options?: LambderCookieOptions): void;
68
- /**
69
- * Adds a Set-Cookie header that deletes the cookie. Pass the same
70
- * `domain` and `path` the cookie was set with: a cookie's identity is
71
- * (name, domain, path), and a deletion under a different scope targets a
72
- * different cookie and deletes nothing.
73
- */
74
- clearCookie(name: string, options?: LambderClearCookieOptions): void;
75
- logToApiResponse(input: unknown): void;
76
45
  raw(init: LambderRawResponseInit): LambderResponse;
77
46
  json(data: Record<string, any>, options?: LambderResponseOptions): LambderResponse;
78
47
  text(data: string, options?: LambderResponseOptions): LambderResponse;
@@ -109,37 +78,13 @@ export default class LambderResponseBuilder<TResponse = any> {
109
78
  templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
110
79
  htmlVirtualSlots?: boolean;
111
80
  }): Promise<LambderResponse>;
112
- api(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
113
- api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
114
81
  /**
115
- * A payload as the API's output schema declares it. The type system
116
- * accepts a value that carries more than the schema (a row read straight
117
- * from a table is assignable to a narrower object type), and without this
118
- * the extra fields, a password hash included, would reach the client.
119
- * zod strips what the schema does not declare, fills its defaults and
120
- * applies its transforms, so the wire and the idempotency store only see
121
- * the declared shape. A refusal's payload beside an errorMessage or a
122
- * flag is parsed the same way; only null passes as it is. Only an API
123
- * handler's own resolver holds the schema: a hook, a validation handler
124
- * or an error handler answers in shapes of its own, a cached answer in
125
- * its wire form, and is sent as given.
126
- *
127
- * The payload is the schema's input form (what a handler writes before
128
- * the transforms), so a transform runs exactly once. A payload the schema
129
- * rejects is a handler breaking its contract, answered as a crash rather
130
- * than sent (LambderApiOutputValidationError, which an idempotency key
131
- * records as its answer, since the handler has already run).
132
- *
133
- * The parse is synchronous, so an output schema cannot be async: zod
134
- * throws from a synchronous parse that meets an async refinement or
135
- * transform, and a transform may throw of its own accord. Either throw
136
- * becomes the same LambderApiOutputValidationError, carrying what was
137
- * thrown as its cause. Left to escape as it is, it would read as the
138
- * handler crashing before its answer: the idempotency engine would
139
- * release the key's claim and every retry would run the operation again.
82
+ * An API envelope written by hand: what a hook, an input validation
83
+ * handler or a global error handler answers an API call with (a refusal
84
+ * flag, an errorMessage, a crash). The payload goes out as given; an API
85
+ * handler's own output is parsed through its schema by the instance
86
+ * instead. The logList channel is what the request accumulated unless the
87
+ * config names its own.
140
88
  */
141
- private declaredPayload;
142
- /** Same as api() but forces compression of the response body. */
143
- apiBinary(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
144
- apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
89
+ api(payload: unknown, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
145
90
  }
@@ -1,18 +1,18 @@
1
- import { serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
2
1
  import { LambderResponse } from "./LambderResponse.js";
3
2
  import { buildApiEnvelope } from "../api/LambderApiEnvelope.js";
4
- import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
3
+ /**
4
+ * Builds the responses of routes, hooks and error handlers. An API handler
5
+ * never holds one: it returns its output or refuses, and writes headers and
6
+ * cookies through its context (LambderResponseTools).
7
+ */
5
8
  export default class LambderResponseBuilder {
6
9
  files;
7
10
  apiVersion;
8
11
  ctx;
9
- /** The output schema of the API this builder answers, which every success payload is parsed through; null outside an API handler. */
10
- apiOutput;
11
- constructor({ files, apiVersion, ctx, apiOutput }) {
12
+ constructor({ files, apiVersion, ctx }) {
12
13
  this.files = files ?? null;
13
14
  this.apiVersion = apiVersion ?? null;
14
15
  this.ctx = ctx;
15
- this.apiOutput = apiOutput ?? null;
16
16
  }
17
17
  ;
18
18
  buildResponse(statusCode, contentType, body, options, defaults) {
@@ -37,49 +37,6 @@ export default class LambderResponseBuilder {
37
37
  throw new Error(`Lambder: ${method} requires the files option at creation (e.g. files: new LambderLocalFileSource({ root }))`);
38
38
  return this.files;
39
39
  }
40
- /** Appends a response header; applied onto the response once the handler has one, in call order. */
41
- addHeader(key, value) {
42
- if (!this.ctx)
43
- throw new Error("Lambder: res.addHeader needs the request context, and this response builder was created without one.");
44
- this.ctx.responseHeaders.add(key, value);
45
- }
46
- ;
47
- /** Replaces a response header; applied onto the response once the handler has one, in call order. */
48
- setHeader(key, value) {
49
- if (!this.ctx)
50
- throw new Error("Lambder: res.setHeader needs the request context, and this response builder was created without one.");
51
- this.ctx.responseHeaders.set(key, value);
52
- }
53
- ;
54
- /**
55
- * Adds a Set-Cookie header. A function-form `domain` is resolved against
56
- * the request hostname. Defaults: Path=/, SameSite=Lax, Secure, not
57
- * HttpOnly, browser-session lifetime.
58
- */
59
- setCookie(name, value, options) {
60
- if (!this.ctx)
61
- throw new Error("Lambder: res.setCookie needs the request context, and this response builder was created without one.");
62
- this.addHeader("Set-Cookie", serializeCookie(name, value, options, this.ctx.host));
63
- }
64
- ;
65
- /**
66
- * Adds a Set-Cookie header that deletes the cookie. Pass the same
67
- * `domain` and `path` the cookie was set with: a cookie's identity is
68
- * (name, domain, path), and a deletion under a different scope targets a
69
- * different cookie and deletes nothing.
70
- */
71
- clearCookie(name, options) {
72
- if (!this.ctx)
73
- throw new Error("Lambder: res.clearCookie needs the request context, and this response builder was created without one.");
74
- this.addHeader("Set-Cookie", serializeClearCookie(name, options, this.ctx.host));
75
- }
76
- ;
77
- logToApiResponse(input) {
78
- if (!this.ctx)
79
- throw new Error("Lambder: res.logToApiResponse needs the request context, and this response builder was created without one.");
80
- this.ctx.logList.push(input);
81
- }
82
- ;
83
40
  raw(init) {
84
41
  return new LambderResponse({
85
42
  statusCode: init.statusCode,
@@ -177,58 +134,17 @@ export default class LambderResponseBuilder {
177
134
  return this.buildResponse(200, "text/html; charset=utf-8", template.render(data), options);
178
135
  }
179
136
  ;
180
- api(payload, config = {}, options) {
181
- // The envelope is the core's (one writer for both the server and the
182
- // mock runtime); the logList channel is what this request accumulated
183
- // unless the config names its own.
184
- const envelope = buildApiEnvelope(this.apiVersion, this.declaredPayload(payload), { ...config, logList: config.logList || this.ctx?.logList });
185
- return this.json(envelope, options);
186
- }
187
- ;
188
137
  /**
189
- * A payload as the API's output schema declares it. The type system
190
- * accepts a value that carries more than the schema (a row read straight
191
- * from a table is assignable to a narrower object type), and without this
192
- * the extra fields, a password hash included, would reach the client.
193
- * zod strips what the schema does not declare, fills its defaults and
194
- * applies its transforms, so the wire and the idempotency store only see
195
- * the declared shape. A refusal's payload beside an errorMessage or a
196
- * flag is parsed the same way; only null passes as it is. Only an API
197
- * handler's own resolver holds the schema: a hook, a validation handler
198
- * or an error handler answers in shapes of its own, a cached answer in
199
- * its wire form, and is sent as given.
200
- *
201
- * The payload is the schema's input form (what a handler writes before
202
- * the transforms), so a transform runs exactly once. A payload the schema
203
- * rejects is a handler breaking its contract, answered as a crash rather
204
- * than sent (LambderApiOutputValidationError, which an idempotency key
205
- * records as its answer, since the handler has already run).
206
- *
207
- * The parse is synchronous, so an output schema cannot be async: zod
208
- * throws from a synchronous parse that meets an async refinement or
209
- * transform, and a transform may throw of its own accord. Either throw
210
- * becomes the same LambderApiOutputValidationError, carrying what was
211
- * thrown as its cause. Left to escape as it is, it would read as the
212
- * handler crashing before its answer: the idempotency engine would
213
- * release the key's claim and every retry would run the operation again.
138
+ * An API envelope written by hand: what a hook, an input validation
139
+ * handler or a global error handler answers an API call with (a refusal
140
+ * flag, an errorMessage, a crash). The payload goes out as given; an API
141
+ * handler's own output is parsed through its schema by the instance
142
+ * instead. The logList channel is what the request accumulated unless the
143
+ * config names its own.
214
144
  */
215
- declaredPayload(payload) {
216
- if (!this.apiOutput || payload === null)
217
- return payload;
218
- const apiName = this.ctx?.apiName ?? "?";
219
- let parsed;
220
- try {
221
- parsed = this.apiOutput.safeParse(payload);
222
- }
223
- catch (thrown) {
224
- throw new LambderApiOutputValidationError(apiName, { thrown });
225
- }
226
- if (parsed.success)
227
- return parsed.data;
228
- throw new LambderApiOutputValidationError(apiName, { zodError: parsed.error });
229
- }
230
- apiBinary(payload, config = {}, options) {
231
- return this.api(payload, config, { ...options, compress: true });
145
+ api(payload, config = {}, options) {
146
+ const envelope = buildApiEnvelope(this.apiVersion, payload, { ...config, logList: config.logList || this.ctx?.logList });
147
+ return this.json(envelope, options);
232
148
  }
233
149
  ;
234
150
  }
package/dist/index.d.ts CHANGED
@@ -30,12 +30,12 @@ export { toHttpAnswer } from "./api/LambderApiAnswer.js";
30
30
  export type { LambderApiAnswer } from "./api/LambderApiAnswer.js";
31
31
  export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader } from "./shared/wire/LambderAnswerHeaders.js";
32
32
  export { createApiCallContext } from "./api/LambderApiCallContext.js";
33
- export type { LambderApiCallContext, LambderApiCallTrace } from "./api/LambderApiCallContext.js";
33
+ export type { LambderApiCallContext, LambderApiCallTrace, LambderResponseTools } from "./api/LambderApiCallContext.js";
34
34
  export type { LambderApiDefinition } from "./api/LambderApiDefinition.js";
35
35
  export { apiSignatureOf } from "./api/LambderApiSignature.js";
36
36
  export type { LambderApiSignatureEntry } from "./api/LambderApiSignature.js";
37
- export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
38
- export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
37
+ export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignatureMap.js";
38
+ export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignatureMap.js";
39
39
  export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
40
40
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
41
41
  export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
@@ -68,7 +68,7 @@ export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
68
68
  export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
69
69
  export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
70
70
  export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
71
- export type { LambderResponseOptions, LambderRawResponseInit, LambderResolverApiMethod, } from "./core/LambderResponseBuilder.js";
71
+ export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
72
72
  export type { LambderRouteMatcher, LambderRouteConditionFn, LambderRouteCondition, LambderPathParamsOf, LambderRoutePath } from "./core/LambderRouting.js";
73
73
  export type { LambderCorsConfig } from "./core/LambderCors.js";
74
74
  export type { LambderCreateOptions, LambderSessionOptions, LambderActionTools, LambderHandler, } from "./core/LambderCreateOptions.js";
@@ -133,7 +133,7 @@ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
133
133
  export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
134
134
  export { createLambderI18n } from "./shared/LambderI18n.js";
135
135
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
136
- export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
136
+ export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
137
137
  export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent, LambderHttpEventFormat } from "./core/LambderContext.js";
138
138
  export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
139
139
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader
24
24
  export { createApiCallContext } from "./api/LambderApiCallContext.js";
25
25
  // Per-endpoint signatures: what a client build ships with, digested from the server's own registrations.
26
26
  export { apiSignatureOf } from "./api/LambderApiSignature.js";
27
- export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
27
+ export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignatureMap.js";
28
28
  export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
29
29
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
30
30
  export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
@@ -99,5 +99,5 @@ export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
99
99
  export { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES,
100
100
  // The Brotli twin of the browser's compressPayloadGzip, and its defaults.
101
101
  DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, compressPayloadBrotli, } from "./shared/wire/LambderRequestPayload.js";
102
- // Cookies (res.setCookie / res.clearCookie build on these; exported for code holding a LambderResponse)
102
+ // Cookies (ctx.setCookie / ctx.clearCookie build on these; exported for code holding a LambderResponse)
103
103
  export { serializeCookie, serializeClearCookie, resolveCookieDomain } from "./shared/wire/LambderCookie.js";
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
24
24
  import { type LambderInvokeFailure, type LambderInvokeOutcome } from "./LambderInvokeOutcome.js";
25
- import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
25
+ import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
26
26
  import type { LambdaClient, LambdaClientConfig } from "@aws-sdk/client-lambda";
27
27
  import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
28
28
  import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from "../shared/wire/LambderCallOptions.js";