lambder 8.3.1 → 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.
@@ -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
  }
@@ -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,7 +30,7 @@ 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";
@@ -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
@@ -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";
@@ -378,10 +378,9 @@ export default class LambderInvokeCaller {
378
378
  // The answer's Set-Cookie values, so a session the callee rotated or
379
379
  // cleared is visible to whoever is carrying it.
380
380
  const cookies = http.cookies;
381
- // The declared output, by the callee's own typing: res.api(null)
382
- // compiles only for an output that allows null or beside a reason (an
383
- // errorMessage is a failure below; a message-only null is the
384
- // callee's contract to keep).
381
+ // The declared output, by the callee's own typing: a handler returns
382
+ // its output, so a success payload is null only where the output
383
+ // allows null.
385
384
  if (outcome.ok)
386
385
  return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList, cookies };
387
386
  const shared = { status: outcome.status, retryAfterSeconds: outcome.retryAfterSeconds, logList, cookies };
@@ -1,6 +1,6 @@
1
1
  import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
2
2
  import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
3
- import { bindCallTools, createApiCallContext } from "../api/LambderApiCallContext.js";
3
+ import { bindCallTools, createApiCallContext, responseToolsOf } from "../api/LambderApiCallContext.js";
4
4
  import { toHttpAnswer } from "../api/LambderApiAnswer.js";
5
5
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
6
6
  import { buildApiEnvelope, envelopeAnswer, crashAnswer, } from "../api/LambderApiEnvelope.js";
@@ -562,7 +562,6 @@ export class LambderMockApp {
562
562
  apiName: request.apiName,
563
563
  request,
564
564
  signal: request.signal ?? new AbortController().signal,
565
- envelope: {},
566
565
  payload: request.payload,
567
566
  guardInputs: request.guardInputs,
568
567
  // The key as a handler can use it. A non-string is not a key: the
@@ -591,6 +590,7 @@ export class LambderMockApp {
591
590
  methods: {
592
591
  rateLimit: async (policy, key) => { await chargeRateLimit(policy, key, true); },
593
592
  isRateLimited: (policy, key) => chargeRateLimit(policy, key, false),
593
+ ...responseToolsOf(ctx, request.host),
594
594
  },
595
595
  });
596
596
  return ctx;
@@ -683,7 +683,6 @@ export class LambderMockApp {
683
683
  callCtx.payload = request.payload;
684
684
  const payload = await handler(callCtx);
685
685
  return envelopeAnswer(buildApiEnvelope(this.apiVersion, payload === undefined ? null : payload, {
686
- message: callCtx.envelope.message,
687
686
  logList: callCtx.logList,
688
687
  }));
689
688
  }
@@ -282,8 +282,8 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
282
282
  };
283
283
  /**
284
284
  * What the server's input validation handler answers, as a mock states it:
285
- * `res.api(payload, config)` as data, with the status it went out with (200
286
- * unless named).
285
+ * the handler's `res.api(payload, config)` as data, with the status it went
286
+ * out with (200 unless named).
287
287
  */
288
288
  export type LambderMockInvalidInputAnswer = {
289
289
  payload?: unknown;
@@ -9,7 +9,7 @@ import type { z } from "zod";
9
9
  import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
10
10
  import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
11
11
  import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck } from "../api/LambderApiRateLimits.js";
12
- import type { LambderApiCallContext } from "../api/LambderApiCallContext.js";
12
+ import type { LambderApiCallContext, LambderResponseTools } from "../api/LambderApiCallContext.js";
13
13
  import type { LambderApiRequest } from "../api/LambderApiRequest.js";
14
14
  import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
15
15
  import type { LambderApiDefinition } from "../api/LambderApiDefinition.js";
@@ -47,7 +47,7 @@ export type LambderMockSurplusKeys<TOptions, TShape> = [
47
47
  * fields: the API core's call context plus the request, a session
48
48
  * controller for the call, and the caller's abort signal.
49
49
  */
50
- export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
50
+ export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & LambderResponseTools & {
51
51
  apiName: string;
52
52
  request: LambderApiRequest;
53
53
  /** Create, rotate, refresh and end sessions, exactly as a server handler does through its own ctx.sessionController. */
@@ -61,15 +61,6 @@ export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
61
61
  /** The same count, answered instead of thrown, as ctx.isRateLimited on the server. */
62
62
  isRateLimited: LambderContextRateLimitCheck<Record<string, LambderApiRateLimitPolicyConfig>>;
63
63
  signal: AbortSignal;
64
- /**
65
- * The envelope fields that travel beside the payload, the mock's stand-in
66
- * for the server's `res.api(payload, config)`: a mock handler returns its
67
- * payload, so this is where the rest of the envelope goes. `logList` is
68
- * the usual channel and lives on the context itself.
69
- */
70
- envelope: {
71
- message?: string;
72
- };
73
64
  };
74
65
  /** The same, with the session present: what a `session: true` mock guard and a session endpoint's handler see. */
75
66
  export type LambderMockSessionCallContext<S = any> = Omit<LambderMockCallContext<S>, "session"> & {
@@ -12,6 +12,24 @@
12
12
  * awaits either.
13
13
  */
14
14
  export type MaybePromise<T> = T | Promise<T>;
15
+ /**
16
+ * T with every object and array readonly, all the way down: what an API
17
+ * handler's returned answer is checked against.
18
+ *
19
+ * An answer is a return value, and TypeScript widens the literals of a
20
+ * return value whose expected type is still generic (`{ kind: "a" }` reads
21
+ * as `{ kind: string }`), where a call argument keeps them. The handler's
22
+ * return is therefore its own `const` type parameter, which keeps literals;
23
+ * `const` also makes array literals readonly, which a schema's mutable arrays
24
+ * would refuse, so the bound is this readonly view of the output. Nothing
25
+ * writes to an answer (it is parsed and sent), so readonly costs nothing.
26
+ * Functions and Dates pass through whole. Eight levels deep and no further,
27
+ * because an output may be recursive (`z.json()`), and past that depth the
28
+ * type is left as it is.
29
+ */
30
+ export type LambderReadonlyDeep<T, TDepth extends unknown[] = []> = TDepth["length"] extends 8 ? T : T extends (...args: never[]) => unknown ? T : T extends Date ? T : T extends object ? {
31
+ readonly [K in keyof T]: LambderReadonlyDeep<T[K], [...TDepth, unknown]>;
32
+ } : T;
15
33
  /**
16
34
  * A declaration map with AT LEAST ONE entry: the union, over every declarable
17
35
  * name, of "this one required and the rest optional".
@@ -21,8 +21,9 @@ export type LambderHeaderTarget = {
21
21
  addHeader(key: string, value: string): unknown;
22
22
  };
23
23
  /**
24
- * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
25
- * the session controller's Set-Cookie), applied onto the answer once the
24
+ * Response headers written while a call runs (`ctx.setResponseHeader`,
25
+ * `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
26
+ * Set-Cookie), applied onto the answer once the
26
27
  * call has one. Recorded as operations in call order rather than as a map,
27
28
  * so `set` replaces what the answer itself carries (a Content-Type, say) and
28
29
  * `add` appends to it, exactly as if called on the answer directly.
@@ -36,8 +36,9 @@ export const addAnswerHeader = (headers, name, value) => {
36
36
  headers[name] = [value];
37
37
  };
38
38
  /**
39
- * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
40
- * the session controller's Set-Cookie), applied onto the answer once the
39
+ * Response headers written while a call runs (`ctx.setResponseHeader`,
40
+ * `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
41
+ * Set-Cookie), applied onto the answer once the
41
42
  * call has one. Recorded as operations in call order rather than as a map,
42
43
  * so `set` replaces what the answer itself carries (a Content-Type, say) and
43
44
  * `add` appends to it, exactly as if called on the answer directly.