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
package/CHANGELOG.md CHANGED
@@ -9,6 +9,96 @@ sit on its first published patch, and later patches list only what they changed.
9
9
  Releases up to 3.2.6 carry git tags; the ones after it were published without
10
10
  one, so versions are not cross-linked to tag comparisons here.
11
11
 
12
+ ## [9.0.2] - 2026-09-27
13
+
14
+ Nothing a handler or a caller does changes; the package, its docs and its
15
+ tests are easier to find one's way in.
16
+
17
+ ### Changed
18
+
19
+ - **The README names the one v9 break the compiler cannot find.** A refusal
20
+ is no longer stored under an idempotency key, so a retry after one runs the
21
+ handler again (the 9.0.1 entry has the detail).
22
+ - **The signature map's module is `shared/wire/LambderApiSignatureMap`.** It
23
+ shared its file name with `api/LambderApiSignature`, which computes the
24
+ digests. The entries export the same names as before, so no import changes.
25
+ - **Docs.** `docs/dynamodb-tables.md` is `docs/ddb-tables.md`, beside the
26
+ other `ddb-*` pages. `docs/responses.md` says that `compress: true` behind a
27
+ REST API needs `binaryMediaTypes: ["*/*"]`, as its compression does.
28
+ - **Tests** sit in folders that mirror `src/`.
29
+
30
+ ## [9.0.1] - 2026-09-27
31
+
32
+ A major that gives an API handler one shape. It takes its context and returns
33
+ its output; it says no with `refuse()`; it writes headers, cookies and log
34
+ entries through the context. The response builder stays with routes, hooks,
35
+ fallbacks and error handlers, which build HTTP responses. An API is typed data
36
+ in and typed data out, and a handler that has no response builder cannot
37
+ answer any other way, so the two ways a handler used to refuse (throwing, or
38
+ answering null beside a reason) are one, and the untyped side channels are
39
+ gone. A server handler and its mock twin now read alike.
40
+
41
+ ### Changed (breaking)
42
+
43
+ - **An API handler returns its output.** `addApi` and `addSessionApi` take
44
+ `async (ctx) => output` instead of `async (ctx, res) => res.api(output)`.
45
+ The return is checked against the output schema's input form and parsed
46
+ through the schema before it is sent, as `res.api()`'s payload was:
47
+ undeclared fields are stripped, defaults filled, transforms run once, and
48
+ an output the schema rejects is answered as a crash
49
+ (`LambderApiOutputValidationError`). A literal in a returned object keeps
50
+ its type (`{ status: "open" }` is checked as `"open"`, not `string`), which
51
+ is why the handler's return is its own `const` type parameter; arrays in a
52
+ returned literal are read as readonly, which is all an answer needs.
53
+ - To move: drop the second parameter and return what was passed to
54
+ `res.api()`. An answer that was `res.api(null, { errorMessage })`,
55
+ `{ notAuthorized }` or `{ sessionExpired }` becomes `refuse(content, {
56
+ notAuthorized, sessionExpired, code, type })`. A handler that answered
57
+ null where the output does not allow null no longer compiles; it refuses
58
+ instead, or the output becomes nullable.
59
+ - **A refusal is never stored for replay.** Only an answer (a returned
60
+ output) is stored under an idempotency key; a refusal is thrown, the claim
61
+ is released, and a retry runs the handler again, which decides afresh. A
62
+ corrected request after a refusal goes through under the same key instead
63
+ of being refused as a reused key.
64
+ - **Response headers and cookies are written through the context.**
65
+ `ctx.setResponseHeader(key, value)`, `ctx.addResponseHeader(key, value)`,
66
+ `ctx.setCookie(name, value, options?)` and `ctx.clearCookie(name,
67
+ options?)` (the type `LambderResponseTools`) are on every context: routes,
68
+ API handlers, hooks, and mock handlers alike. `res.setHeader`,
69
+ `res.addHeader`, `res.setCookie` and `res.clearCookie` are removed. The
70
+ writers say "Response" because `ctx.header(name)` reads a request header.
71
+ - **The log channel is `ctx.logList`.** `res.logToApiResponse(entry)` is
72
+ removed; push the entry onto `ctx.logList`, which the mock's context has
73
+ always had.
74
+ - **Answer compression is declared per API.** `addApi` and `addSessionApi`
75
+ take `compress?: boolean | "auto"` beside the other options: "auto" (the
76
+ default) compresses for an accepting caller when the body is large enough,
77
+ false never (an answer carrying base64 bytes), true always. It covers
78
+ refusals and replayed answers too, and is a transport setting of the
79
+ server, not part of the contract. `res.apiBinary()` and the per-answer
80
+ response options of an API handler (status code, compression, cache
81
+ headers) are removed; a header goes through `ctx.setResponseHeader`.
82
+ - **`res.api()` writes an envelope by hand, for code outside an API
83
+ handler.** A hook, the input validation handler or a global error handler
84
+ answering an API call still uses it; the payload goes out as given. The
85
+ typed overloads and `LambderResolver`'s output type parameter are removed,
86
+ with the types `LambderResolverApiMethod` and `LambderApiNullAnswerConfig`.
87
+ A binary or file answer is a route's job.
88
+
89
+ ### Removed
90
+
91
+ - **The `message` envelope channel.** The untyped `message` field of the
92
+ envelope and of `LambderApiResponseConfig`, `LambderCaller`'s
93
+ `messageHandler` option and per-call override, and the mock's
94
+ `ctx.envelope`. What a success says belongs in the output schema, where it
95
+ is typed and parsed.
96
+
97
+ ### Added
98
+
99
+ - `LambderResponseTools`, the type of the four response writers every
100
+ context carries, exported from `lambder`.
101
+
12
102
  ## [8.3.1] - 2026-09-27
13
103
 
14
104
  Six additions, each a thing an app otherwise writes for itself once per kind
package/README.md CHANGED
@@ -17,15 +17,17 @@ const lambder = initLambder<SessionData>().create({
17
17
  }).addApi("getCompany", {
18
18
  input: z.object({ slug: z.string() }),
19
19
  output: z.object({ id: z.string(), name: z.string() }),
20
- }, async ({ apiPayload }, res) => res.api(await loadCompany(apiPayload.slug)));
20
+ }, async ({ apiPayload }) => await loadCompany(apiPayload.slug));
21
21
 
22
22
  export type ApiContractType = typeof lambder.ApiContract;
23
23
  export const handler = lambder.getHandler();
24
24
  ```
25
25
 
26
- Registration chains onto the creation call: every `addApi` returns an instance
27
- carrying the contract so far, so the whole backend is one declaration and
28
- `lambder.ApiContract` is the accumulated type.
26
+ A handler returns its output, which is parsed through the output schema before
27
+ it is sent, and says no by throwing `refuse()`. Registration chains onto the
28
+ creation call: every `addApi` returns an instance carrying the contract so
29
+ far, so the whole backend is one declaration and `lambder.ApiContract` is the
30
+ accumulated type.
29
31
 
30
32
  The frontend imports that contract type and gets autocomplete, typed payloads
31
33
  and typed results with no hand-written client:
@@ -153,7 +155,7 @@ guide that matches what you are building. The full index lives in
153
155
  | [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
154
156
  | [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, crash reporting, and non-HTTP invocations |
155
157
  | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal`, the signature file |
156
- | [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
158
+ | [Responses](./docs/responses.md) | The render context and its response tools (headers, cookies, log entries), the response builder routes and hooks use, compression, ETag and the size cap |
157
159
  | [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
158
160
  | [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
159
161
  | [Calling another lambda](./docs/invoke.md) | `LambderInvokeCaller`: invoking a Lambder app in another function, its contract, failures and compression |
@@ -165,7 +167,7 @@ guide that matches what you are building. The full index lives in
165
167
  | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
166
168
  | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
167
169
  | [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
168
- | [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
170
+ | [DynamoDB tables](./docs/ddb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
169
171
  | [Exports reference](./docs/exports.md) | Every name the five entry points export, grouped by purpose |
170
172
 
171
173
  ## Standalone modules
@@ -185,25 +187,16 @@ framework:
185
187
  ## Versioning and changes
186
188
 
187
189
  Released versions and what each one changed are in
188
- [CHANGELOG.md](./CHANGELOG.md). The current major is v8, which came out of a
189
- review of 7.3.1: session writes that cannot undo a logout, API calls that must
190
- be JSON, output schemas applied at runtime, rate limits that count IPv6 callers
191
- by their /64 and custom keys after the guards, idempotency keys bound to the
192
- request they were first sent with, and the same behavior on every gateway and
193
- in the mock. Every break and what to do about it is in the 8.0.2 entry. The
194
- compiler finds most of them. Fourteen it cannot are named there: hand-built
195
- calls without a JSON Content-Type, hand-built answers without `apiVersion`,
196
- handlers whose payload does not match their output schema, code that decoded
197
- `ctx.path` itself, string routes that match case-sensitively, compression
198
- behind a REST API, two more IAM actions (`UpdateItem` on the session table,
199
- `GetItem` on the rate-limit table), custom-key limits charged after the
200
- guards, idempotency keys bound to the request they were first sent with,
201
- `errorMessage` always being an object, `credentials: true` needing named
202
- origins, template slots and `html` interpolations refused or checked in
203
- more attribute positions, `refreshSessionData()` throwing where it answered
204
- null, and `z.ZodType<T>` annotations leaving a field unchecked. Every live
205
- session is signed out once by the
206
- upgrade. An app still on v6 goes through the 7.0.0 entry first.
190
+ [CHANGELOG.md](./CHANGELOG.md). The current major is v9, which gives an API
191
+ handler one shape: it takes its context and returns its output, says no with
192
+ `refuse()`, and writes headers, cookies and log entries through the context,
193
+ while the response builder stays with routes, hooks and error handlers. Every
194
+ break and what to do about it is in the 9.0.1 entry, and the compiler finds
195
+ most of them. The one it cannot is a change of behavior: a refusal is no
196
+ longer stored under an idempotency key, so a retry after one runs the handler
197
+ again. An app still on v7 goes through the 8.0.2 entry first, which
198
+ names the breaks of v8 the compiler cannot find, and one on v6 through the
199
+ 7.0.0 entry before that.
207
200
 
208
201
  ## Contributing
209
202
 
@@ -1,5 +1,6 @@
1
1
  import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
2
2
  import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
3
+ import { type LambderCookieOptions, type LambderClearCookieOptions } from "../shared/wire/LambderCookie.js";
3
4
  /**
4
5
  * The context the API core needs from whoever runs it. The server's render
5
6
  * context and the mock runtime's handler context both extend it; the
@@ -12,7 +13,7 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
12
13
  * - `responseHeaders` collects headers written during the call, applied
13
14
  * onto the answer by the pipeline.
14
15
  * - `logList` collects entries for the envelope's logList channel
15
- * (`res.logToApiResponse` on the server).
16
+ * (`ctx.logList.push(entry)` from a handler).
16
17
  */
17
18
  export type LambderApiCallContext<TSessionData = any> = {
18
19
  session: LambderSessionRecord<TSessionData> | null;
@@ -22,6 +23,35 @@ export type LambderApiCallContext<TSessionData = any> = {
22
23
  };
23
24
  /** A fresh call context: no session, no guard data, nothing pending. */
24
25
  export declare const createApiCallContext: <TSessionData = any>() => LambderApiCallContext<TSessionData>;
26
+ /**
27
+ * What a handler writes onto its answer beside the body: headers and cookies,
28
+ * collected on `responseHeaders` and applied to whatever answer the request
29
+ * ends with. On every context a handler receives, the server's and the
30
+ * mock's alike, since an API handler returns its output and has no response
31
+ * builder to write them on.
32
+ */
33
+ export type LambderResponseTools = {
34
+ /** Replaces a response header. */
35
+ setResponseHeader(key: string, value: string | string[]): void;
36
+ /** Appends a response header value (repeatable for one key). */
37
+ addResponseHeader(key: string, value: string): void;
38
+ /**
39
+ * Adds a Set-Cookie header. A function-form `domain` is resolved against
40
+ * the request host. Defaults: Path=/, SameSite=Lax, Secure, not HttpOnly,
41
+ * browser-session lifetime.
42
+ */
43
+ setCookie(name: string, value: string, options?: LambderCookieOptions): void;
44
+ /**
45
+ * Adds a Set-Cookie header that deletes the cookie. Pass the `domain` and
46
+ * `path` it was set with: a cookie's identity is (name, domain, path), and
47
+ * a deletion under another scope deletes nothing.
48
+ */
49
+ clearCookie(name: string, options?: LambderClearCookieOptions): void;
50
+ };
51
+ /** The response tools of one context, writing into its own responseHeaders; `host` resolves a function-form cookie domain. */
52
+ export declare const responseToolsOf: (ctx: {
53
+ responseHeaders: LambderAnswerHeaders;
54
+ }, host: string) => LambderResponseTools;
25
55
  /**
26
56
  * Binds an adapter's tools onto one call context: `getters` run when read
27
57
  * (`ctx.sessionController` is built over the object it was read from),
@@ -1,4 +1,5 @@
1
1
  import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
2
+ import { serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
2
3
  /** A fresh call context: no session, no guard data, nothing pending. */
3
4
  export const createApiCallContext = () => ({
4
5
  session: null,
@@ -10,6 +11,13 @@ export const createApiCallContext = () => ({
10
11
  responseHeaders: new LambderAnswerHeaders(),
11
12
  logList: [],
12
13
  });
14
+ /** The response tools of one context, writing into its own responseHeaders; `host` resolves a function-form cookie domain. */
15
+ export const responseToolsOf = (ctx, host) => ({
16
+ setResponseHeader: (key, value) => { ctx.responseHeaders.set(key, value); },
17
+ addResponseHeader: (key, value) => { ctx.responseHeaders.add(key, value); },
18
+ setCookie: (name, value, options) => { ctx.responseHeaders.add("Set-Cookie", serializeCookie(name, value, options, host)); },
19
+ clearCookie: (name, options) => { ctx.responseHeaders.add("Set-Cookie", serializeClearCookie(name, options, host)); },
20
+ });
13
21
  /**
14
22
  * Binds an adapter's tools onto one call context: `getters` run when read
15
23
  * (`ctx.sessionController` is built over the object it was read from),
@@ -8,8 +8,8 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
8
8
  * because the mock has none; when input is present, validation runs and the
9
9
  * handler sees the parsed payload. Output is part of the endpoint's
10
10
  * signature (apiSignatureOf), which is what a client's build is checked
11
- * against; the server's resolver also parses every payload a handler
12
- * answers through it before it is sent.
11
+ * against; the server also parses every output a handler returns through it
12
+ * before it is sent.
13
13
  */
14
14
  export type LambderApiDefinition = {
15
15
  name: string;
@@ -13,7 +13,7 @@ export type LambderApiEnvelopeConfig = LambderApiResponseConfig & {
13
13
  * plain success is `{ apiVersion, payload }` and nothing else; an empty
14
14
  * logList is omitted.
15
15
  */
16
- export declare const buildApiEnvelope: <T>(apiVersion: string | null | undefined, payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, crash, }?: LambderApiEnvelopeConfig) => LambderApiEnvelopeBody<T>;
16
+ export declare const buildApiEnvelope: <T>(apiVersion: string | null | undefined, payload: T | null, { versionExpired, sessionExpired, notAuthorized, errorMessage, logList, crash, }?: LambderApiEnvelopeConfig) => LambderApiEnvelopeBody<T>;
17
17
  /** An envelope as an answer: JSON body, JSON content type, the status and headers given (200 and none by default). */
18
18
  export declare const envelopeAnswer: (envelope: LambderApiEnvelopeBody<unknown>, options?: {
19
19
  statusCode?: number;
@@ -4,8 +4,8 @@ import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
4
4
  * The one place the API envelope is written, and the one mapping from each
5
5
  * kind of protocol outcome onto an answer: a success, a thrown refusal, a
6
6
  * rejected input, an unknown name, a missing session, a stale client, a
7
- * malformed compressed payload, and the last-resort crash. The server's
8
- * res.api(), the mock runtime and the pipeline all build through these
7
+ * malformed compressed payload, and the last-resort crash. The server's API
8
+ * path and res.api(), the mock runtime and the pipeline all build through these
9
9
  * functions, so server and mock cannot drift on a single byte of the wire
10
10
  * format. Pure: no Node built-ins, no response classes.
11
11
  */
@@ -15,7 +15,7 @@ export const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
15
15
  * plain success is `{ apiVersion, payload }` and nothing else; an empty
16
16
  * logList is omitted.
17
17
  */
18
- export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, crash, } = {}) => ({
18
+ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionExpired, notAuthorized, errorMessage, logList, crash, } = {}) => ({
19
19
  apiVersion: apiVersion ?? null,
20
20
  payload,
21
21
  ...(versionExpired ? { versionExpired } : {}),
@@ -26,7 +26,6 @@ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionE
26
26
  // flags above are booleans, where false and absent mean the same. An
27
27
  // errorMessage goes out as a message object whatever form it was written
28
28
  // in, so every reader meets one shape.
29
- ...(message !== undefined ? { message } : {}),
30
29
  ...(errorMessage !== undefined ? { errorMessage: refusalMessageOf(errorMessage) } : {}),
31
30
  ...(crash !== undefined ? { crash } : {}),
32
31
  ...(logList?.length ? { logList } : {}),
@@ -386,13 +386,11 @@ export class LambderApiIdempotencyEngine {
386
386
  }
387
387
  // A crash or a thrown refusal (LambderApiRefusal, refuse())
388
388
  // releases the claim so a retry retries. The rule is deliberate:
389
- // ANSWERS are stored and replayed, returned refusal envelopes
390
- // included; EXCEPTIONS are not, so a thrown refusal re-executes
391
- // on retry and the handler decides afresh. (On the server,
392
- // res.die.api() is an answer: the adapter catches it before it
393
- // reaches here.) The handler's own error is what gets rethrown,
394
- // since a cleanup error in its place would hide why the call
395
- // failed.
389
+ // ANSWERS (the output a handler returned) are stored and
390
+ // replayed; EXCEPTIONS are not, so a thrown refusal re-executes
391
+ // on retry and the handler decides afresh. The handler's own
392
+ // error is what gets rethrown, since a cleanup error in its place
393
+ // would hide why the call failed.
396
394
  try {
397
395
  await store.abandon(scopeKey, ownerToken);
398
396
  }
@@ -4,7 +4,7 @@ import type { LambderApiAnswer } from "./LambderApiAnswer.js";
4
4
  import type { LambderApiCallContext } from "./LambderApiCallContext.js";
5
5
  import type { LambderApiCallTrace } from "./LambderApiCallContext.js";
6
6
  import type { LambderApiDefinition } from "./LambderApiDefinition.js";
7
- import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
7
+ import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
8
8
  import { type LambderApiGuard } from "./LambderApiGuards.js";
9
9
  import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig, type LambderRateLimitChargeResult, type LambderRateLimitChargeSubject } from "./LambderApiRateLimits.js";
10
10
  import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
@@ -1,5 +1,5 @@
1
1
  import { restoreCompressedPayload } from "./LambderApiRequest.js";
2
- import { lookupApiSignature } from "../shared/wire/LambderApiSignature.js";
2
+ import { lookupApiSignature } from "../shared/wire/LambderApiSignatureMap.js";
3
3
  import { apiNotFoundAnswer, invalidPayloadAnswer, refusalAnswer, sessionExpiredAnswer, validationAnswer, versionExpiredAnswer, } from "./LambderApiEnvelope.js";
4
4
  import { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./LambderApiValidationRefusal.js";
5
5
  import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { toGuardEntries } from "./LambderApiGuards.js";
3
- import { API_SIGNATURE_HEX_LENGTH, EXTENSIBLE_ENUM_META_KEY } from "../shared/wire/LambderApiSignature.js";
3
+ import { API_SIGNATURE_HEX_LENGTH, EXTENSIBLE_ENUM_META_KEY } from "../shared/wire/LambderApiSignatureMap.js";
4
4
  import { sha256HexOf } from "../shared/util/LambderTextDigest.js";
5
5
  import { canonicalJson } from "../shared/util/canonicalJson.js";
6
6
  /*
@@ -4,7 +4,7 @@ import type { LambderApiContractShape } from '../shared/wire/LambderApiContract.
4
4
  import { type LambderApiOutcome, type LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
5
5
  import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from '../shared/wire/LambderCallOptions.js';
6
6
  import { type LambderApiTransport } from '../shared/transport/LambderApiTransport.js';
7
- import { type LambderApiSignatureMap } from '../shared/wire/LambderApiSignature.js';
7
+ import { type LambderApiSignatureMap } from '../shared/wire/LambderApiSignatureMap.js';
8
8
  export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
9
9
  export type { LambderProvidedGuardInputs, LambderGuardInputsProvider } from '../shared/wire/LambderCallOptions.js';
10
10
  /** A handler told that something happened, with nothing to hand it. */
@@ -29,7 +29,6 @@ type FetchEndEventHandler = (params: {
29
29
  }) => void | Promise<void>;
30
30
  type ErrorHandler = (err: Error) => void | Promise<void>;
31
31
  type ValidationErrorHandler = (zodError: LambderValidationError) => (void | false) | Promise<(void | false)>;
32
- type MessageHandler = (message: LambderAppRefusalMessage | string) => void | Promise<void>;
33
32
  /** Handed the refusal as its message object, a plain-string errorMessage having been read as one (refusalMessageOf). */
34
33
  type ErrorMessageHandler = (message: LambderAppRefusalMessage) => void | Promise<void>;
35
34
  /** The logListHandler option: an answer's logList, success or failure, when it has entries. The invoke caller's onLogList, for a browser. */
@@ -41,7 +40,6 @@ export type LambderLogListHandler = (apiName: string, logList: unknown[]) => voi
41
40
  export type LambderCallOptions = LambderSharedCallOptions & {
42
41
  versionExpiredHandler?: NotifyHandler;
43
42
  sessionExpiredHandler?: NotifyHandler;
44
- messageHandler?: MessageHandler;
45
43
  errorMessageHandler?: ErrorMessageHandler;
46
44
  apiInputValidationErrorHandler?: ValidationErrorHandler;
47
45
  notAuthorizedHandler?: NotifyHandler;
@@ -73,7 +71,6 @@ type LambderCallerBaseOptions = {
73
71
  timeoutMs?: number;
74
72
  versionExpiredHandler?: NotifyHandler;
75
73
  sessionExpiredHandler?: NotifyHandler;
76
- messageHandler?: MessageHandler;
77
74
  errorMessageHandler?: ErrorMessageHandler;
78
75
  notAuthorizedHandler?: NotifyHandler;
79
76
  errorHandler?: ErrorHandler;
@@ -119,7 +116,6 @@ export default class LambderCaller<TContract extends LambderApiContractShape = a
119
116
  get isLoading(): boolean;
120
117
  private versionExpiredHandler?;
121
118
  private sessionExpiredHandler?;
122
- private messageHandler?;
123
119
  private errorMessageHandler?;
124
120
  private notAuthorizedHandler?;
125
121
  private errorHandler?;
@@ -8,7 +8,7 @@ import { createCallAbort } from '../shared/util/LambderCallAbort.js';
8
8
  import { coerceToError } from '../shared/wire/LambderCrashDetail.js';
9
9
  import { isLambderTransportFailure } from '../shared/transport/LambderApiTransport.js';
10
10
  import { DEFAULT_SESSION_TOKEN_COOKIE_KEY, DEFAULT_SESSION_CSRF_COOKIE_KEY } from '../shared/wire/LambderSessionCookieNames.js';
11
- import { readApiSignature } from '../shared/wire/LambderApiSignature.js';
11
+ import { readApiSignature } from '../shared/wire/LambderApiSignatureMap.js';
12
12
  import { LambderReloadLoopBreaker, RELOAD_LOOP_WINDOW_MS } from './LambderReloadLoopBreaker.js';
13
13
  import { lambderFetchTransport } from './lambderFetchTransport.js';
14
14
  /**
@@ -33,7 +33,6 @@ export default class LambderCaller {
33
33
  get isLoading() { return this.fetchTrackerList.length > 0; }
34
34
  versionExpiredHandler;
35
35
  sessionExpiredHandler;
36
- messageHandler;
37
36
  errorMessageHandler;
38
37
  notAuthorizedHandler;
39
38
  errorHandler;
@@ -50,7 +49,7 @@ export default class LambderCaller {
50
49
  constructor(options) {
51
50
  // The conditional provider option is resolved per instantiation;
52
51
  // inside the class it is read through the plain shape.
53
- const { apiPath, apiVersion, apiSignatures, isCorsEnabled, timeoutMs, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, logListHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, requestCompression, guardInputsProvider, transport, } = options;
52
+ const { apiPath, apiVersion, apiSignatures, isCorsEnabled, timeoutMs, versionExpiredHandler, sessionExpiredHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, logListHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, requestCompression, guardInputsProvider, transport, } = options;
54
53
  this.apiPath = apiPath;
55
54
  this.apiVersion = apiVersion;
56
55
  this.apiSignatures = apiSignatures;
@@ -61,7 +60,6 @@ export default class LambderCaller {
61
60
  this.transport = transport ?? lambderFetchTransport({ cors: isCorsEnabled });
62
61
  this.versionExpiredHandler = versionExpiredHandler;
63
62
  this.sessionExpiredHandler = sessionExpiredHandler;
64
- this.messageHandler = messageHandler;
65
63
  this.errorMessageHandler = errorMessageHandler;
66
64
  this.notAuthorizedHandler = notAuthorizedHandler;
67
65
  this.errorHandler = errorHandler;
@@ -100,7 +98,6 @@ export default class LambderCaller {
100
98
  // Per-call overrides win over the constructor handlers.
101
99
  const versionExpiredHandler = options?.versionExpiredHandler ?? this.versionExpiredHandler;
102
100
  const sessionExpiredHandler = options?.sessionExpiredHandler ?? this.sessionExpiredHandler;
103
- const messageHandler = options?.messageHandler ?? this.messageHandler;
104
101
  const errorMessageHandler = options?.errorMessageHandler ?? this.errorMessageHandler;
105
102
  const notAuthorizedHandler = options?.notAuthorizedHandler ?? this.notAuthorizedHandler;
106
103
  const errorHandler = options?.errorHandler ?? this.errorHandler;
@@ -328,11 +325,6 @@ export default class LambderCaller {
328
325
  }
329
326
  return outcome;
330
327
  }
331
- // Presence, not truthiness: the envelope keeps a message an app
332
- // spelled out as the empty string, so the handler runs for it.
333
- if (data.message !== undefined && messageHandler) {
334
- await messageHandler(data.message);
335
- }
336
328
  if (!outcome.ok && outcome.reason === 'errorMessage') {
337
329
  if (errorMessageHandler && outcome.errorMessage !== undefined) {
338
330
  await errorMessageHandler(outcome.errorMessage);
package/dist/client.d.ts CHANGED
@@ -15,8 +15,8 @@ export type { LambderApiTransport, LambderApiTransportRequest, LambderTransportF
15
15
  export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
16
16
  export type { LambderStoredCookie } from "./shared/transport/LambderCookieJar.js";
17
17
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
18
- export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
19
- export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
18
+ export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignatureMap.js";
19
+ export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignatureMap.js";
20
20
  export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
21
21
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
22
22
  export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCook
16
16
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
17
17
  // The per-endpoint signature map a build ships with, how a caller reads it, the
18
18
  // mark a shared schema sets on an enum its readers let grow, and the reload-loop window.
19
- export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
19
+ export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignatureMap.js";
20
20
  export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
21
21
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
22
22
  export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
@@ -13,7 +13,7 @@ import { type LambderPipelineBackends, type LambderPipelineBackendSwap } from ".
13
13
  import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
14
14
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
15
15
  import { type LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
16
- import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
16
+ import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
17
17
  import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
18
18
  import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
19
19
  import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf } from "../api/LambderApiGuards.js";
@@ -21,7 +21,7 @@ import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "..
21
21
  import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
22
22
  import type { LambderContractEntry, LambderJsonOutputOf, LambderMergeContract } from "../shared/wire/LambderApiContract.js";
23
23
  import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
24
- import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
24
+ import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
25
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";
26
26
  /** Everything `lambder/testing` may put under a built instance: the pipeline's stores, and the source its files are read from. */
27
27
  export type LambderInstanceBackends = LambderPipelineBackends & {
@@ -155,7 +155,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
155
155
  addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
156
156
  addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TSessionData, LambderPathParamsOf<TPath>, {}, _TRateLimitPolicies>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
157
157
  addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TSessionData> & LambderSessionEnabledInstance<_TSessionsEnabled>): this;
158
- 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: {
159
159
  input: TInput;
160
160
  output: TOutput;
161
161
  } & {
@@ -163,8 +163,18 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
163
163
  rateLimit?: TRateOpt;
164
164
  /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
165
165
  idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
166
- } & 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>;
167
- 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: {
168
178
  input: TInput;
169
179
  output: TOutput;
170
180
  } & {
@@ -172,7 +182,17 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
172
182
  rateLimit?: TRateOpt;
173
183
  /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
174
184
  idempotency?: _TIdempotencyEnabled extends true ? TIdempotencyOpt : never;
175
- } & 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>;
176
196
  /**
177
197
  * What registering an API is, for addApi and addSessionApi alike: the
178
198
  * checks that can refuse it, then its definition recorded (what
@@ -267,7 +287,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
267
287
  * and never moves when registrations are reordered.
268
288
  */
269
289
  apiOptionEntries(): LambderApiOptionEntries;
270
- getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder<any>;
290
+ getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder;
271
291
  private getResolver;
272
292
  getHandler(): LambderHandler;
273
293
  /** True when the Lambda event is an API Gateway HTTP event (REST API v1 or HTTP API / Function URL v2). */
@@ -365,12 +385,30 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
365
385
  private inputValidationRefusal;
366
386
  /**
367
387
  * One API call through the core: the pipeline runs the protocol steps and
368
- * calls back for the handler, whose LambderResponse (returned, or thrown
369
- * via res.die.*) becomes the answer the pipeline stores and hands back.
370
- * The context is the pipeline's context, so a session it fetched is on
371
- * ctx.session and the validated payload is on ctx.apiPayload when the
372
- * handler runs. The handler's resolver knows the API's output schema, so
373
- * 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.
374
412
  */
375
413
  private runApi;
376
414
  /** A thrown LambderApiRefusal (from a hook, say) as the structured API envelope: the core's one mapping. */