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.
- package/CHANGELOG.md +90 -0
- package/README.md +18 -25
- package/dist/api/LambderApiCallContext.d.ts +31 -1
- package/dist/api/LambderApiCallContext.js +8 -0
- package/dist/api/LambderApiDefinition.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +3 -4
- package/dist/api/LambderApiIdempotency.js +5 -7
- package/dist/api/LambderApiPipeline.d.ts +1 -1
- package/dist/api/LambderApiPipeline.js +1 -1
- package/dist/api/LambderApiSignature.js +1 -1
- package/dist/client/LambderCaller.d.ts +1 -5
- package/dist/client/LambderCaller.js +2 -10
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/core/Lambder.d.ts +51 -13
- package/dist/core/Lambder.js +48 -39
- package/dist/core/LambderContext.d.ts +9 -6
- package/dist/core/LambderContext.js +2 -1
- package/dist/core/LambderCreateOptions.d.ts +1 -1
- package/dist/core/LambderResolver.d.ts +6 -12
- package/dist/core/LambderResolver.js +2 -14
- package/dist/core/LambderResponseBuilder.d.ts +15 -70
- package/dist/core/LambderResponseBuilder.js +15 -99
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/invoke/LambderInvokeCaller.d.ts +1 -1
- package/dist/invoke/LambderInvokeCaller.js +4 -5
- package/dist/mock/LambderMockApp.js +2 -3
- package/dist/mock/LambderMockCreateOptions.d.ts +3 -3
- package/dist/mock/LambderMockTypes.d.ts +2 -11
- package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
- package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
- package/dist/shared/wire/LambderApiContract.d.ts +9 -14
- package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
- package/dist/shared/wire/LambderApiRefusal.js +3 -4
- package/dist/testing/LambderTestVisitor.d.ts +1 -1
- package/package.json +1 -1
- /package/dist/shared/wire/{LambderApiSignature.d.ts → LambderApiSignatureMap.d.ts} +0 -0
- /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 }
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
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
|
|
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/
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
* (`
|
|
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
|
|
12
|
-
*
|
|
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,
|
|
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,
|
|
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
|
|
390
|
-
//
|
|
391
|
-
// on retry and the handler decides afresh.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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,
|
|
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/
|
|
19
|
-
export type { LambderApiSignatureMap } from "./shared/wire/
|
|
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/
|
|
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";
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
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. */
|