lambder 8.3.1 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +15 -24
- 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/client/LambderCaller.d.ts +0 -4
- package/dist/client/LambderCaller.js +1 -9
- package/dist/core/Lambder.d.ts +50 -12
- package/dist/core/Lambder.js +47 -38
- package/dist/core/LambderContext.d.ts +9 -6
- package/dist/core/LambderContext.js +2 -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 +3 -3
- package/dist/index.js +1 -1
- package/dist/invoke/LambderInvokeCaller.js +3 -4
- package/dist/mock/LambderMockApp.js +2 -3
- package/dist/mock/LambderMockCreateOptions.d.ts +2 -2
- 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/package.json +1 -1
|
@@ -2,6 +2,7 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyEvent
|
|
|
2
2
|
import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
|
|
3
3
|
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
4
4
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
5
|
+
import { type LambderResponseTools } from "../api/LambderApiCallContext.js";
|
|
5
6
|
import type LambderSessionController from "../session/LambderSessionController.js";
|
|
6
7
|
import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult } from "../api/LambderApiRateLimits.js";
|
|
7
8
|
export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
@@ -19,7 +20,9 @@ export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxy
|
|
|
19
20
|
* API core's call context (session, guardData, responseHeaders, logList),
|
|
20
21
|
* which is the part the pipeline and the session controller work on; the
|
|
21
22
|
* rest is the HTTP request as the Lambda event delivered it, plus the tools
|
|
22
|
-
* the instance rendering it binds on (sessionController, rateLimit, isRateLimited)
|
|
23
|
+
* the instance rendering it binds on (sessionController, rateLimit, isRateLimited)
|
|
24
|
+
* and the response tools (setResponseHeader, addResponseHeader, setCookie,
|
|
25
|
+
* clearCookie) that write onto whatever answer the request ends with.
|
|
23
26
|
*
|
|
24
27
|
* TRateLimitPolicies is the app's policies map on a handler registered with
|
|
25
28
|
* addApi, addSessionApi, addRoute or addSessionRoute, so a policy name is
|
|
@@ -102,9 +105,9 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
102
105
|
lambdaContext: Context;
|
|
103
106
|
/** Which API Gateway payload format the event arrived in, and the response leaves in. */
|
|
104
107
|
eventFormat: LambderHttpEventFormat;
|
|
105
|
-
/** Response headers written during the request (
|
|
108
|
+
/** Response headers written during the request (the response tools below, session cookies), applied onto the response at the end. */
|
|
106
109
|
responseHeaders: LambderAnswerHeaders;
|
|
107
|
-
/** Entries for the API envelope's logList channel
|
|
110
|
+
/** Entries for the API envelope's logList channel: a handler pushes what it wants the caller's debug log to show. */
|
|
108
111
|
logList: unknown[];
|
|
109
112
|
/**
|
|
110
113
|
* Sessions for this request: read the one it carries
|
|
@@ -125,12 +128,12 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
125
128
|
rateLimit: LambderContextRateLimit<TRateLimitPolicies>;
|
|
126
129
|
/** The same count as rateLimit, answered instead of thrown: false, or the window that refused and its retryAfterSeconds. */
|
|
127
130
|
isRateLimited: LambderContextRateLimitCheck<TRateLimitPolicies>;
|
|
128
|
-
};
|
|
131
|
+
} & LambderResponseTools;
|
|
129
132
|
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData, SessionData, TRateLimitPolicies>, 'session'> & {
|
|
130
133
|
session: LambderSessionRecord<SessionData>;
|
|
131
134
|
};
|
|
132
|
-
/** The members of a render context that
|
|
133
|
-
type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited";
|
|
135
|
+
/** The members of a render context that are bound onto it rather than read from its event. */
|
|
136
|
+
type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited" | keyof LambderResponseTools;
|
|
134
137
|
/** What an instance binds onto each context it renders: see bindContextTools. */
|
|
135
138
|
export type LambderContextTools = {
|
|
136
139
|
sessionControllerFor: (ctx: LambderRenderContext) => LambderSessionController<any>;
|
|
@@ -4,7 +4,7 @@ import { base64ToText } from "../shared/util/LambderBase64.js";
|
|
|
4
4
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
5
5
|
import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
|
|
6
6
|
import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
|
|
7
|
-
import { bindCallTools } from "../api/LambderApiCallContext.js";
|
|
7
|
+
import { bindCallTools, responseToolsOf } from "../api/LambderApiCallContext.js";
|
|
8
8
|
import { decodeRequestPath } from "./LambderRequestPath.js";
|
|
9
9
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
10
10
|
export const isV2HttpEvent = (event) => !!event && typeof event === "object"
|
|
@@ -23,6 +23,7 @@ export const bindContextTools = (ctx, tools) => {
|
|
|
23
23
|
methods: {
|
|
24
24
|
rateLimit: async (policy, key) => { await tools.chargeRateLimit(bound, policy, key, true); },
|
|
25
25
|
isRateLimited: (policy, key) => tools.chargeRateLimit(bound, policy, key, false),
|
|
26
|
+
...responseToolsOf(bound, bound.host),
|
|
26
27
|
},
|
|
27
28
|
});
|
|
28
29
|
return bound;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import LambderResponseBuilder, { type LambderResolverApiMethod, type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
|
|
1
|
+
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
3
2
|
import type { LambderResponse } from "./LambderResponse.js";
|
|
4
3
|
type SyncDie<T extends (...args: any[]) => LambderResponse> = (...args: Parameters<T>) => never;
|
|
5
4
|
type AsyncDie<T extends (...args: any[]) => Promise<LambderResponse>> = (...args: Parameters<T>) => Promise<never>;
|
|
6
5
|
/** The `res.die.*` surface: every builder method, throwing what it built. Internal to the resolver, which is the only thing that has one. */
|
|
7
|
-
interface DieResolverMethods
|
|
6
|
+
interface DieResolverMethods {
|
|
8
7
|
raw: SyncDie<LambderResponseBuilder["raw"]>;
|
|
9
8
|
json: SyncDie<LambderResponseBuilder["json"]>;
|
|
10
9
|
text: SyncDie<LambderResponseBuilder["text"]>;
|
|
@@ -15,25 +14,20 @@ interface DieResolverMethods<TOutput> {
|
|
|
15
14
|
redirect: SyncDie<LambderResponseBuilder["redirect"]>;
|
|
16
15
|
versionExpired: SyncDie<LambderResponseBuilder["versionExpired"]>;
|
|
17
16
|
fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
|
|
18
|
-
api:
|
|
19
|
-
apiBinary: LambderResolverApiMethod<TOutput, never>;
|
|
17
|
+
api: SyncDie<LambderResponseBuilder["api"]>;
|
|
20
18
|
file: AsyncDie<LambderResponseBuilder["file"]>;
|
|
21
19
|
templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
|
-
* Response builder passed to route
|
|
22
|
+
* Response builder passed to route handlers and hooks.
|
|
25
23
|
*
|
|
26
24
|
* `res.die.*` builds the response and THROWS it, immediately halting the
|
|
27
25
|
* request at any call depth (handlers, hooks, nested service functions).
|
|
28
26
|
* Lambder's render pipeline catches thrown LambderResponse instances and uses
|
|
29
27
|
* them as the response. Plain `throw res.html(...)` works the same way.
|
|
30
28
|
*/
|
|
31
|
-
export default class LambderResolver
|
|
32
|
-
die: DieResolverMethods
|
|
29
|
+
export default class LambderResolver extends LambderResponseBuilder {
|
|
30
|
+
die: DieResolverMethods;
|
|
33
31
|
constructor(...args: ConstructorParameters<typeof LambderResponseBuilder>);
|
|
34
|
-
api(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
35
|
-
api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
36
|
-
apiBinary(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
37
|
-
apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
38
32
|
}
|
|
39
33
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
2
2
|
/**
|
|
3
|
-
* Response builder passed to route
|
|
3
|
+
* Response builder passed to route handlers and hooks.
|
|
4
4
|
*
|
|
5
5
|
* `res.die.*` builds the response and THROWS it, immediately halting the
|
|
6
6
|
* request at any call depth (handlers, hooks, nested service functions).
|
|
@@ -22,21 +22,9 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
22
22
|
redirect: (...a) => { throw this.redirect(...a); },
|
|
23
23
|
versionExpired: (...a) => { throw this.versionExpired(...a); },
|
|
24
24
|
fileBase64: (...a) => { throw this.fileBase64(...a); },
|
|
25
|
-
|
|
26
|
-
api: ((payload, config, options) => {
|
|
27
|
-
throw this.api(payload, config, options);
|
|
28
|
-
}),
|
|
29
|
-
apiBinary: ((payload, config, options) => {
|
|
30
|
-
throw this.apiBinary(payload, config, options);
|
|
31
|
-
}),
|
|
25
|
+
api: (...a) => { throw this.api(...a); },
|
|
32
26
|
file: async (...a) => { throw await this.file(...a); },
|
|
33
27
|
templateFile: async (...a) => { throw await this.templateFile(...a); },
|
|
34
28
|
};
|
|
35
29
|
}
|
|
36
|
-
api(payload, config, options) {
|
|
37
|
-
return super.api(payload, config, options);
|
|
38
|
-
}
|
|
39
|
-
apiBinary(payload, config, options) {
|
|
40
|
-
return super.apiBinary(payload, config, options);
|
|
41
|
-
}
|
|
42
30
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
3
|
-
import { type LambderCookieOptions, type LambderClearCookieOptions } from "../shared/wire/LambderCookie.js";
|
|
4
2
|
import type { LambderFiles } from "./LambderFiles.js";
|
|
5
3
|
import { LambderResponse, type LambderHeadersInput } from "./LambderResponse.js";
|
|
6
4
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
7
5
|
import { LambderSafeHtml } from "../shared/LambderHtml.js";
|
|
8
6
|
import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
9
|
-
import type { LambderApiResponseConfig
|
|
7
|
+
import type { LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
10
8
|
export type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
11
9
|
export type LambderResponseOptions = {
|
|
12
10
|
statusCode?: LambderHttpStatusCode;
|
|
@@ -18,19 +16,6 @@ export type LambderResponseOptions = {
|
|
|
18
16
|
/** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
|
|
19
17
|
etag?: boolean | "auto";
|
|
20
18
|
};
|
|
21
|
-
/**
|
|
22
|
-
* The two shapes of an API answer: the output the contract declares, or
|
|
23
|
-
* `null` beside a config that says why (a refusal flag, an `errorMessage`, a
|
|
24
|
-
* `message`). A bare `res.api(null)` compiles only when the output type
|
|
25
|
-
* itself allows null, so a success payload is always the declared output,
|
|
26
|
-
* which lets a typed caller (LambderInvokeCaller.api) promise it. Untyped
|
|
27
|
-
* resolvers (`TOutput = any`) accept anything. This is the resolver's method
|
|
28
|
-
* type; the core's answer type is LambderApiAnswer.
|
|
29
|
-
*/
|
|
30
|
-
export type LambderResolverApiMethod<TOutput, TResult> = {
|
|
31
|
-
(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): TResult;
|
|
32
|
-
(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): TResult;
|
|
33
|
-
};
|
|
34
19
|
export type LambderRawResponseInit = {
|
|
35
20
|
statusCode: LambderHttpStatusCode;
|
|
36
21
|
headers?: LambderHeadersInput;
|
|
@@ -40,39 +25,23 @@ export type LambderRawResponseInit = {
|
|
|
40
25
|
compress?: boolean | "auto";
|
|
41
26
|
etag?: boolean | "auto";
|
|
42
27
|
};
|
|
43
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Builds the responses of routes, hooks and error handlers. An API handler
|
|
30
|
+
* never holds one: it returns its output or refuses, and writes headers and
|
|
31
|
+
* cookies through its context (LambderResponseTools).
|
|
32
|
+
*/
|
|
33
|
+
export default class LambderResponseBuilder {
|
|
44
34
|
protected files: LambderFiles | null;
|
|
45
35
|
protected apiVersion: string | null;
|
|
46
36
|
protected ctx?: LambderRenderContext;
|
|
47
|
-
|
|
48
|
-
protected apiOutput: z.ZodType | null;
|
|
49
|
-
constructor({ files, apiVersion, ctx, apiOutput }: {
|
|
37
|
+
constructor({ files, apiVersion, ctx }: {
|
|
50
38
|
files?: LambderFiles | null;
|
|
51
39
|
apiVersion?: string | null;
|
|
52
40
|
ctx?: LambderRenderContext;
|
|
53
|
-
apiOutput?: z.ZodType;
|
|
54
41
|
});
|
|
55
42
|
private buildResponse;
|
|
56
43
|
/** The instance's file reader, which res.file and res.templateFile need. */
|
|
57
44
|
private requireFiles;
|
|
58
|
-
/** Appends a response header; applied onto the response once the handler has one, in call order. */
|
|
59
|
-
addHeader(key: string, value: string): void;
|
|
60
|
-
/** Replaces a response header; applied onto the response once the handler has one, in call order. */
|
|
61
|
-
setHeader(key: string, value: string | string[]): void;
|
|
62
|
-
/**
|
|
63
|
-
* Adds a Set-Cookie header. A function-form `domain` is resolved against
|
|
64
|
-
* the request hostname. Defaults: Path=/, SameSite=Lax, Secure, not
|
|
65
|
-
* HttpOnly, browser-session lifetime.
|
|
66
|
-
*/
|
|
67
|
-
setCookie(name: string, value: string, options?: LambderCookieOptions): void;
|
|
68
|
-
/**
|
|
69
|
-
* Adds a Set-Cookie header that deletes the cookie. Pass the same
|
|
70
|
-
* `domain` and `path` the cookie was set with: a cookie's identity is
|
|
71
|
-
* (name, domain, path), and a deletion under a different scope targets a
|
|
72
|
-
* different cookie and deletes nothing.
|
|
73
|
-
*/
|
|
74
|
-
clearCookie(name: string, options?: LambderClearCookieOptions): void;
|
|
75
|
-
logToApiResponse(input: unknown): void;
|
|
76
45
|
raw(init: LambderRawResponseInit): LambderResponse;
|
|
77
46
|
json(data: Record<string, any>, options?: LambderResponseOptions): LambderResponse;
|
|
78
47
|
text(data: string, options?: LambderResponseOptions): LambderResponse;
|
|
@@ -109,37 +78,13 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
109
78
|
templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
|
|
110
79
|
htmlVirtualSlots?: boolean;
|
|
111
80
|
}): Promise<LambderResponse>;
|
|
112
|
-
api(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
113
|
-
api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
114
81
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* the declared shape. A refusal's payload beside an errorMessage or a
|
|
122
|
-
* flag is parsed the same way; only null passes as it is. Only an API
|
|
123
|
-
* handler's own resolver holds the schema: a hook, a validation handler
|
|
124
|
-
* or an error handler answers in shapes of its own, a cached answer in
|
|
125
|
-
* its wire form, and is sent as given.
|
|
126
|
-
*
|
|
127
|
-
* The payload is the schema's input form (what a handler writes before
|
|
128
|
-
* the transforms), so a transform runs exactly once. A payload the schema
|
|
129
|
-
* rejects is a handler breaking its contract, answered as a crash rather
|
|
130
|
-
* than sent (LambderApiOutputValidationError, which an idempotency key
|
|
131
|
-
* records as its answer, since the handler has already run).
|
|
132
|
-
*
|
|
133
|
-
* The parse is synchronous, so an output schema cannot be async: zod
|
|
134
|
-
* throws from a synchronous parse that meets an async refinement or
|
|
135
|
-
* transform, and a transform may throw of its own accord. Either throw
|
|
136
|
-
* becomes the same LambderApiOutputValidationError, carrying what was
|
|
137
|
-
* thrown as its cause. Left to escape as it is, it would read as the
|
|
138
|
-
* handler crashing before its answer: the idempotency engine would
|
|
139
|
-
* release the key's claim and every retry would run the operation again.
|
|
82
|
+
* An API envelope written by hand: what a hook, an input validation
|
|
83
|
+
* handler or a global error handler answers an API call with (a refusal
|
|
84
|
+
* flag, an errorMessage, a crash). The payload goes out as given; an API
|
|
85
|
+
* handler's own output is parsed through its schema by the instance
|
|
86
|
+
* instead. The logList channel is what the request accumulated unless the
|
|
87
|
+
* config names its own.
|
|
140
88
|
*/
|
|
141
|
-
|
|
142
|
-
/** Same as api() but forces compression of the response body. */
|
|
143
|
-
apiBinary(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
144
|
-
apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
|
|
89
|
+
api(payload: unknown, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
145
90
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
|
|
2
1
|
import { LambderResponse } from "./LambderResponse.js";
|
|
3
2
|
import { buildApiEnvelope } from "../api/LambderApiEnvelope.js";
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Builds the responses of routes, hooks and error handlers. An API handler
|
|
5
|
+
* never holds one: it returns its output or refuses, and writes headers and
|
|
6
|
+
* cookies through its context (LambderResponseTools).
|
|
7
|
+
*/
|
|
5
8
|
export default class LambderResponseBuilder {
|
|
6
9
|
files;
|
|
7
10
|
apiVersion;
|
|
8
11
|
ctx;
|
|
9
|
-
|
|
10
|
-
apiOutput;
|
|
11
|
-
constructor({ files, apiVersion, ctx, apiOutput }) {
|
|
12
|
+
constructor({ files, apiVersion, ctx }) {
|
|
12
13
|
this.files = files ?? null;
|
|
13
14
|
this.apiVersion = apiVersion ?? null;
|
|
14
15
|
this.ctx = ctx;
|
|
15
|
-
this.apiOutput = apiOutput ?? null;
|
|
16
16
|
}
|
|
17
17
|
;
|
|
18
18
|
buildResponse(statusCode, contentType, body, options, defaults) {
|
|
@@ -37,49 +37,6 @@ export default class LambderResponseBuilder {
|
|
|
37
37
|
throw new Error(`Lambder: ${method} requires the files option at creation (e.g. files: new LambderLocalFileSource({ root }))`);
|
|
38
38
|
return this.files;
|
|
39
39
|
}
|
|
40
|
-
/** Appends a response header; applied onto the response once the handler has one, in call order. */
|
|
41
|
-
addHeader(key, value) {
|
|
42
|
-
if (!this.ctx)
|
|
43
|
-
throw new Error("Lambder: res.addHeader needs the request context, and this response builder was created without one.");
|
|
44
|
-
this.ctx.responseHeaders.add(key, value);
|
|
45
|
-
}
|
|
46
|
-
;
|
|
47
|
-
/** Replaces a response header; applied onto the response once the handler has one, in call order. */
|
|
48
|
-
setHeader(key, value) {
|
|
49
|
-
if (!this.ctx)
|
|
50
|
-
throw new Error("Lambder: res.setHeader needs the request context, and this response builder was created without one.");
|
|
51
|
-
this.ctx.responseHeaders.set(key, value);
|
|
52
|
-
}
|
|
53
|
-
;
|
|
54
|
-
/**
|
|
55
|
-
* Adds a Set-Cookie header. A function-form `domain` is resolved against
|
|
56
|
-
* the request hostname. Defaults: Path=/, SameSite=Lax, Secure, not
|
|
57
|
-
* HttpOnly, browser-session lifetime.
|
|
58
|
-
*/
|
|
59
|
-
setCookie(name, value, options) {
|
|
60
|
-
if (!this.ctx)
|
|
61
|
-
throw new Error("Lambder: res.setCookie needs the request context, and this response builder was created without one.");
|
|
62
|
-
this.addHeader("Set-Cookie", serializeCookie(name, value, options, this.ctx.host));
|
|
63
|
-
}
|
|
64
|
-
;
|
|
65
|
-
/**
|
|
66
|
-
* Adds a Set-Cookie header that deletes the cookie. Pass the same
|
|
67
|
-
* `domain` and `path` the cookie was set with: a cookie's identity is
|
|
68
|
-
* (name, domain, path), and a deletion under a different scope targets a
|
|
69
|
-
* different cookie and deletes nothing.
|
|
70
|
-
*/
|
|
71
|
-
clearCookie(name, options) {
|
|
72
|
-
if (!this.ctx)
|
|
73
|
-
throw new Error("Lambder: res.clearCookie needs the request context, and this response builder was created without one.");
|
|
74
|
-
this.addHeader("Set-Cookie", serializeClearCookie(name, options, this.ctx.host));
|
|
75
|
-
}
|
|
76
|
-
;
|
|
77
|
-
logToApiResponse(input) {
|
|
78
|
-
if (!this.ctx)
|
|
79
|
-
throw new Error("Lambder: res.logToApiResponse needs the request context, and this response builder was created without one.");
|
|
80
|
-
this.ctx.logList.push(input);
|
|
81
|
-
}
|
|
82
|
-
;
|
|
83
40
|
raw(init) {
|
|
84
41
|
return new LambderResponse({
|
|
85
42
|
statusCode: init.statusCode,
|
|
@@ -177,58 +134,17 @@ export default class LambderResponseBuilder {
|
|
|
177
134
|
return this.buildResponse(200, "text/html; charset=utf-8", template.render(data), options);
|
|
178
135
|
}
|
|
179
136
|
;
|
|
180
|
-
api(payload, config = {}, options) {
|
|
181
|
-
// The envelope is the core's (one writer for both the server and the
|
|
182
|
-
// mock runtime); the logList channel is what this request accumulated
|
|
183
|
-
// unless the config names its own.
|
|
184
|
-
const envelope = buildApiEnvelope(this.apiVersion, this.declaredPayload(payload), { ...config, logList: config.logList || this.ctx?.logList });
|
|
185
|
-
return this.json(envelope, options);
|
|
186
|
-
}
|
|
187
|
-
;
|
|
188
137
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* the declared shape. A refusal's payload beside an errorMessage or a
|
|
196
|
-
* flag is parsed the same way; only null passes as it is. Only an API
|
|
197
|
-
* handler's own resolver holds the schema: a hook, a validation handler
|
|
198
|
-
* or an error handler answers in shapes of its own, a cached answer in
|
|
199
|
-
* its wire form, and is sent as given.
|
|
200
|
-
*
|
|
201
|
-
* The payload is the schema's input form (what a handler writes before
|
|
202
|
-
* the transforms), so a transform runs exactly once. A payload the schema
|
|
203
|
-
* rejects is a handler breaking its contract, answered as a crash rather
|
|
204
|
-
* than sent (LambderApiOutputValidationError, which an idempotency key
|
|
205
|
-
* records as its answer, since the handler has already run).
|
|
206
|
-
*
|
|
207
|
-
* The parse is synchronous, so an output schema cannot be async: zod
|
|
208
|
-
* throws from a synchronous parse that meets an async refinement or
|
|
209
|
-
* transform, and a transform may throw of its own accord. Either throw
|
|
210
|
-
* becomes the same LambderApiOutputValidationError, carrying what was
|
|
211
|
-
* thrown as its cause. Left to escape as it is, it would read as the
|
|
212
|
-
* handler crashing before its answer: the idempotency engine would
|
|
213
|
-
* release the key's claim and every retry would run the operation again.
|
|
138
|
+
* An API envelope written by hand: what a hook, an input validation
|
|
139
|
+
* handler or a global error handler answers an API call with (a refusal
|
|
140
|
+
* flag, an errorMessage, a crash). The payload goes out as given; an API
|
|
141
|
+
* handler's own output is parsed through its schema by the instance
|
|
142
|
+
* instead. The logList channel is what the request accumulated unless the
|
|
143
|
+
* config names its own.
|
|
214
144
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const apiName = this.ctx?.apiName ?? "?";
|
|
219
|
-
let parsed;
|
|
220
|
-
try {
|
|
221
|
-
parsed = this.apiOutput.safeParse(payload);
|
|
222
|
-
}
|
|
223
|
-
catch (thrown) {
|
|
224
|
-
throw new LambderApiOutputValidationError(apiName, { thrown });
|
|
225
|
-
}
|
|
226
|
-
if (parsed.success)
|
|
227
|
-
return parsed.data;
|
|
228
|
-
throw new LambderApiOutputValidationError(apiName, { zodError: parsed.error });
|
|
229
|
-
}
|
|
230
|
-
apiBinary(payload, config = {}, options) {
|
|
231
|
-
return this.api(payload, config, { ...options, compress: true });
|
|
145
|
+
api(payload, config = {}, options) {
|
|
146
|
+
const envelope = buildApiEnvelope(this.apiVersion, payload, { ...config, logList: config.logList || this.ctx?.logList });
|
|
147
|
+
return this.json(envelope, options);
|
|
232
148
|
}
|
|
233
149
|
;
|
|
234
150
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { toHttpAnswer } from "./api/LambderApiAnswer.js";
|
|
|
30
30
|
export type { LambderApiAnswer } from "./api/LambderApiAnswer.js";
|
|
31
31
|
export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader } from "./shared/wire/LambderAnswerHeaders.js";
|
|
32
32
|
export { createApiCallContext } from "./api/LambderApiCallContext.js";
|
|
33
|
-
export type { LambderApiCallContext, LambderApiCallTrace } from "./api/LambderApiCallContext.js";
|
|
33
|
+
export type { LambderApiCallContext, LambderApiCallTrace, LambderResponseTools } from "./api/LambderApiCallContext.js";
|
|
34
34
|
export type { LambderApiDefinition } from "./api/LambderApiDefinition.js";
|
|
35
35
|
export { apiSignatureOf } from "./api/LambderApiSignature.js";
|
|
36
36
|
export type { LambderApiSignatureEntry } from "./api/LambderApiSignature.js";
|
|
@@ -68,7 +68,7 @@ export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
|
|
|
68
68
|
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
|
|
69
69
|
export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
|
|
70
70
|
export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
|
|
71
|
-
export type { LambderResponseOptions, LambderRawResponseInit,
|
|
71
|
+
export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
|
|
72
72
|
export type { LambderRouteMatcher, LambderRouteConditionFn, LambderRouteCondition, LambderPathParamsOf, LambderRoutePath } from "./core/LambderRouting.js";
|
|
73
73
|
export type { LambderCorsConfig } from "./core/LambderCors.js";
|
|
74
74
|
export type { LambderCreateOptions, LambderSessionOptions, LambderActionTools, LambderHandler, } from "./core/LambderCreateOptions.js";
|
|
@@ -133,7 +133,7 @@ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
|
|
|
133
133
|
export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
|
|
134
134
|
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
135
135
|
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
136
|
-
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig,
|
|
136
|
+
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
137
137
|
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent, LambderHttpEventFormat } from "./core/LambderContext.js";
|
|
138
138
|
export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
|
|
139
139
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
package/dist/index.js
CHANGED
|
@@ -99,5 +99,5 @@ export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
|
|
|
99
99
|
export { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES,
|
|
100
100
|
// The Brotli twin of the browser's compressPayloadGzip, and its defaults.
|
|
101
101
|
DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, compressPayloadBrotli, } from "./shared/wire/LambderRequestPayload.js";
|
|
102
|
-
// Cookies (
|
|
102
|
+
// Cookies (ctx.setCookie / ctx.clearCookie build on these; exported for code holding a LambderResponse)
|
|
103
103
|
export { serializeCookie, serializeClearCookie, resolveCookieDomain } from "./shared/wire/LambderCookie.js";
|
|
@@ -378,10 +378,9 @@ export default class LambderInvokeCaller {
|
|
|
378
378
|
// The answer's Set-Cookie values, so a session the callee rotated or
|
|
379
379
|
// cleared is visible to whoever is carrying it.
|
|
380
380
|
const cookies = http.cookies;
|
|
381
|
-
// The declared output, by the callee's own typing:
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
// callee's contract to keep).
|
|
381
|
+
// The declared output, by the callee's own typing: a handler returns
|
|
382
|
+
// its output, so a success payload is null only where the output
|
|
383
|
+
// allows null.
|
|
385
384
|
if (outcome.ok)
|
|
386
385
|
return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList, cookies };
|
|
387
386
|
const shared = { status: outcome.status, retryAfterSeconds: outcome.retryAfterSeconds, logList, cookies };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
2
2
|
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
3
|
-
import { bindCallTools, createApiCallContext } from "../api/LambderApiCallContext.js";
|
|
3
|
+
import { bindCallTools, createApiCallContext, responseToolsOf } from "../api/LambderApiCallContext.js";
|
|
4
4
|
import { toHttpAnswer } from "../api/LambderApiAnswer.js";
|
|
5
5
|
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
6
6
|
import { buildApiEnvelope, envelopeAnswer, crashAnswer, } from "../api/LambderApiEnvelope.js";
|
|
@@ -562,7 +562,6 @@ export class LambderMockApp {
|
|
|
562
562
|
apiName: request.apiName,
|
|
563
563
|
request,
|
|
564
564
|
signal: request.signal ?? new AbortController().signal,
|
|
565
|
-
envelope: {},
|
|
566
565
|
payload: request.payload,
|
|
567
566
|
guardInputs: request.guardInputs,
|
|
568
567
|
// The key as a handler can use it. A non-string is not a key: the
|
|
@@ -591,6 +590,7 @@ export class LambderMockApp {
|
|
|
591
590
|
methods: {
|
|
592
591
|
rateLimit: async (policy, key) => { await chargeRateLimit(policy, key, true); },
|
|
593
592
|
isRateLimited: (policy, key) => chargeRateLimit(policy, key, false),
|
|
593
|
+
...responseToolsOf(ctx, request.host),
|
|
594
594
|
},
|
|
595
595
|
});
|
|
596
596
|
return ctx;
|
|
@@ -683,7 +683,6 @@ export class LambderMockApp {
|
|
|
683
683
|
callCtx.payload = request.payload;
|
|
684
684
|
const payload = await handler(callCtx);
|
|
685
685
|
return envelopeAnswer(buildApiEnvelope(this.apiVersion, payload === undefined ? null : payload, {
|
|
686
|
-
message: callCtx.envelope.message,
|
|
687
686
|
logList: callCtx.logList,
|
|
688
687
|
}));
|
|
689
688
|
}
|
|
@@ -282,8 +282,8 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
|
|
|
282
282
|
};
|
|
283
283
|
/**
|
|
284
284
|
* What the server's input validation handler answers, as a mock states it:
|
|
285
|
-
* `res.api(payload, config)` as data, with the status it went
|
|
286
|
-
* unless named).
|
|
285
|
+
* the handler's `res.api(payload, config)` as data, with the status it went
|
|
286
|
+
* out with (200 unless named).
|
|
287
287
|
*/
|
|
288
288
|
export type LambderMockInvalidInputAnswer = {
|
|
289
289
|
payload?: unknown;
|
|
@@ -9,7 +9,7 @@ import type { z } from "zod";
|
|
|
9
9
|
import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
10
10
|
import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
11
11
|
import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck } from "../api/LambderApiRateLimits.js";
|
|
12
|
-
import type { LambderApiCallContext } from "../api/LambderApiCallContext.js";
|
|
12
|
+
import type { LambderApiCallContext, LambderResponseTools } from "../api/LambderApiCallContext.js";
|
|
13
13
|
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
14
14
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
15
15
|
import type { LambderApiDefinition } from "../api/LambderApiDefinition.js";
|
|
@@ -47,7 +47,7 @@ export type LambderMockSurplusKeys<TOptions, TShape> = [
|
|
|
47
47
|
* fields: the API core's call context plus the request, a session
|
|
48
48
|
* controller for the call, and the caller's abort signal.
|
|
49
49
|
*/
|
|
50
|
-
export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
|
|
50
|
+
export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & LambderResponseTools & {
|
|
51
51
|
apiName: string;
|
|
52
52
|
request: LambderApiRequest;
|
|
53
53
|
/** Create, rotate, refresh and end sessions, exactly as a server handler does through its own ctx.sessionController. */
|
|
@@ -61,15 +61,6 @@ export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
|
|
|
61
61
|
/** The same count, answered instead of thrown, as ctx.isRateLimited on the server. */
|
|
62
62
|
isRateLimited: LambderContextRateLimitCheck<Record<string, LambderApiRateLimitPolicyConfig>>;
|
|
63
63
|
signal: AbortSignal;
|
|
64
|
-
/**
|
|
65
|
-
* The envelope fields that travel beside the payload, the mock's stand-in
|
|
66
|
-
* for the server's `res.api(payload, config)`: a mock handler returns its
|
|
67
|
-
* payload, so this is where the rest of the envelope goes. `logList` is
|
|
68
|
-
* the usual channel and lives on the context itself.
|
|
69
|
-
*/
|
|
70
|
-
envelope: {
|
|
71
|
-
message?: string;
|
|
72
|
-
};
|
|
73
64
|
};
|
|
74
65
|
/** The same, with the session present: what a `session: true` mock guard and a session endpoint's handler see. */
|
|
75
66
|
export type LambderMockSessionCallContext<S = any> = Omit<LambderMockCallContext<S>, "session"> & {
|
|
@@ -12,6 +12,24 @@
|
|
|
12
12
|
* awaits either.
|
|
13
13
|
*/
|
|
14
14
|
export type MaybePromise<T> = T | Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* T with every object and array readonly, all the way down: what an API
|
|
17
|
+
* handler's returned answer is checked against.
|
|
18
|
+
*
|
|
19
|
+
* An answer is a return value, and TypeScript widens the literals of a
|
|
20
|
+
* return value whose expected type is still generic (`{ kind: "a" }` reads
|
|
21
|
+
* as `{ kind: string }`), where a call argument keeps them. The handler's
|
|
22
|
+
* return is therefore its own `const` type parameter, which keeps literals;
|
|
23
|
+
* `const` also makes array literals readonly, which a schema's mutable arrays
|
|
24
|
+
* would refuse, so the bound is this readonly view of the output. Nothing
|
|
25
|
+
* writes to an answer (it is parsed and sent), so readonly costs nothing.
|
|
26
|
+
* Functions and Dates pass through whole. Eight levels deep and no further,
|
|
27
|
+
* because an output may be recursive (`z.json()`), and past that depth the
|
|
28
|
+
* type is left as it is.
|
|
29
|
+
*/
|
|
30
|
+
export type LambderReadonlyDeep<T, TDepth extends unknown[] = []> = TDepth["length"] extends 8 ? T : T extends (...args: never[]) => unknown ? T : T extends Date ? T : T extends object ? {
|
|
31
|
+
readonly [K in keyof T]: LambderReadonlyDeep<T[K], [...TDepth, unknown]>;
|
|
32
|
+
} : T;
|
|
15
33
|
/**
|
|
16
34
|
* A declaration map with AT LEAST ONE entry: the union, over every declarable
|
|
17
35
|
* name, of "this one required and the rest optional".
|
|
@@ -21,8 +21,9 @@ export type LambderHeaderTarget = {
|
|
|
21
21
|
addHeader(key: string, value: string): unknown;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Response headers written while a call runs (`
|
|
25
|
-
* the session controller's
|
|
24
|
+
* Response headers written while a call runs (`ctx.setResponseHeader`,
|
|
25
|
+
* `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
|
|
26
|
+
* Set-Cookie), applied onto the answer once the
|
|
26
27
|
* call has one. Recorded as operations in call order rather than as a map,
|
|
27
28
|
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
28
29
|
* `add` appends to it, exactly as if called on the answer directly.
|
|
@@ -36,8 +36,9 @@ export const addAnswerHeader = (headers, name, value) => {
|
|
|
36
36
|
headers[name] = [value];
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
|
-
* Response headers written while a call runs (`
|
|
40
|
-
* the session controller's
|
|
39
|
+
* Response headers written while a call runs (`ctx.setResponseHeader`,
|
|
40
|
+
* `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
|
|
41
|
+
* Set-Cookie), applied onto the answer once the
|
|
41
42
|
* call has one. Recorded as operations in call order rather than as a map,
|
|
42
43
|
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
43
44
|
* `add` appends to it, exactly as if called on the answer directly.
|