lambder 5.1.2 → 6.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/README.md +8 -3
- package/dist/client/LambderCaller.d.ts +8 -83
- package/dist/client/LambderCaller.js +32 -64
- package/dist/client/LambderMSW.js +4 -4
- package/dist/client.d.ts +5 -3
- package/dist/client.js +3 -1
- package/dist/core/Lambder.d.ts +2 -2
- package/dist/core/Lambder.js +10 -3
- package/dist/core/LambderContext.d.ts +6 -5
- package/dist/core/LambderContext.js +20 -12
- package/dist/core/LambderResolver.d.ts +7 -5
- package/dist/core/LambderResolver.js +7 -3
- package/dist/core/LambderResponseBuilder.d.ts +17 -3
- package/dist/core/LambderResponseBuilder.js +2 -2
- package/dist/index.d.ts +10 -6
- package/dist/index.js +6 -2
- package/dist/invoke/LambderInvokeCaller.d.ts +322 -0
- package/dist/invoke/LambderInvokeCaller.js +654 -0
- package/dist/policies/LambderApiGuards.d.ts +8 -8
- package/dist/policies/LambderApiRateLimits.d.ts +3 -3
- package/dist/session/LambderSessionManager.d.ts +5 -1
- package/dist/session/LambderSessionManager.js +26 -13
- package/dist/shared/LambderApiContract.d.ts +9 -0
- package/dist/shared/LambderApiOutcome.d.ts +69 -0
- package/dist/shared/LambderApiOutcome.js +79 -0
- package/dist/shared/LambderCallOptions.d.ts +71 -0
- package/dist/shared/LambderCallOptions.js +16 -0
- package/dist/shared/LambderCompressionCodec.d.ts +45 -8
- package/dist/shared/LambderCompressionCodec.js +44 -14
- package/dist/shared/LambderCrashDetail.d.ts +48 -0
- package/dist/shared/LambderCrashDetail.js +78 -0
- package/dist/shared/LambderRequestPayload.d.ts +46 -23
- package/dist/shared/LambderRequestPayload.js +47 -31
- package/dist/stores/LambderDdbCache.d.ts +7 -2
- package/dist/stores/LambderDdbCache.js +30 -12
- package/dist/stores/LambderDdbIdempotency.d.ts +7 -2
- package/dist/stores/LambderDdbIdempotency.js +26 -10
- package/dist/stores/LambderDdbRateLimiter.d.ts +7 -2
- package/dist/stores/LambderDdbRateLimiter.js +16 -4
- package/dist/stores/LambderDdbSdk.d.ts +20 -0
- package/dist/stores/LambderDdbSdk.js +31 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -53,6 +53,9 @@ const company = await caller.api("getCompany", { slug: "acme" });
|
|
|
53
53
|
cookies, and a guard against Lambda's response size cap.
|
|
54
54
|
- **Hooks and actions.** Lifecycle hooks, plus `addAction()` for the non-HTTP
|
|
55
55
|
invocations (EventBridge, SQS, custom events) the same function receives.
|
|
56
|
+
- **Lambda to lambda calls.** `LambderInvokeCaller` invokes a Lambder app in
|
|
57
|
+
another function directly, with no API Gateway in between, typed from the
|
|
58
|
+
callee's own contract and carrying its refusals, crash detail and logs back.
|
|
56
59
|
- **Frontend hosting.** Serve a build from a folder, S3, R2 or any HTTP
|
|
57
60
|
origin, with an app shell rendered through a build-pipeline-safe template
|
|
58
61
|
engine.
|
|
@@ -74,8 +77,9 @@ import needs:
|
|
|
74
77
|
| --- | --- |
|
|
75
78
|
| `lambder/client` (browser, shared isomorphic code) | `zod` |
|
|
76
79
|
| `lambder` on AWS Lambda (`nodejs18.x` and later) | `zod`. The runtime already provides the AWS SDK v3, so mark the SDK packages as dev dependencies and keep them out of the deployment package |
|
|
77
|
-
| `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, `@aws-sdk/client-dynamodb
|
|
80
|
+
| `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` when sessions or the DynamoDB stores are used; both are loaded on the first table access, so an app that uses neither needs neither |
|
|
78
81
|
| `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
|
|
82
|
+
| `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
|
|
79
83
|
| `lambder/testing` | `msw` |
|
|
80
84
|
|
|
81
85
|
The SDK and its `@smithy` tree are roughly 21MB installed, which is why they are
|
|
@@ -101,8 +105,8 @@ tree-shaking.
|
|
|
101
105
|
|
|
102
106
|
Source layout mirrors this: `src/core/` (request pipeline), `src/policies/`
|
|
103
107
|
(declarative rate limits, guards, idempotency), `src/session/`, `src/stores/`
|
|
104
|
-
(DynamoDB primitives), `src/client/`,
|
|
105
|
-
both entries re-export).
|
|
108
|
+
(DynamoDB primitives), `src/client/`, `src/invoke/` (the lambda-to-lambda
|
|
109
|
+
caller), and `src/shared/` (isomorphic modules both entries re-export).
|
|
106
110
|
|
|
107
111
|
## Documentation
|
|
108
112
|
|
|
@@ -119,6 +123,7 @@ guide that matches what you are building. The full index lives in
|
|
|
119
123
|
| [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
|
|
120
124
|
| [Sessions](./docs/sessions.md) | DynamoDB sessions, cookie scope, secrets at rest, `dataRefresh`, the controller API |
|
|
121
125
|
| [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
|
|
126
|
+
| [Calling another lambda](./docs/invoke.md) | `LambderInvokeCaller`: invoking a Lambder app in another function, its contract, failures and compression |
|
|
122
127
|
| [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression |
|
|
123
128
|
| [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
|
|
124
129
|
| [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
|
|
@@ -1,57 +1,9 @@
|
|
|
1
1
|
import { type LambderRequestCompressionOption } from '../shared/LambderRequestPayload.js';
|
|
2
|
-
import type { LambderApiResponse } from '../shared/LambderApiContract.js';
|
|
3
2
|
import type { ApiContractShape } from '../shared/LambderApiContract.js';
|
|
4
|
-
import type
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
} ? G : never;
|
|
9
|
-
/** Input type of guard G on one contract entry; never when that API does not declare it. */
|
|
10
|
-
type GuardInputOf<TEntry, G extends string> = GuardInputsOf<TEntry> extends infer I ? (G extends keyof I ? I[G] : never) : never;
|
|
11
|
-
/**
|
|
12
|
-
* What guardInputsProvider returns: for every provided guard name, the value
|
|
13
|
-
* the contract's APIs expect for it (a union across APIs when they differ).
|
|
14
|
-
* Naming a guard no API declares in guardInput mode resolves to never, so a
|
|
15
|
-
* typo fails the provider's return type instead of going missing at runtime.
|
|
16
|
-
*/
|
|
17
|
-
export type LambderProvidedGuardInputs<TContract, TProvided extends string> = IsAny<TContract> extends true ? Record<TProvided, unknown> : {
|
|
18
|
-
[G in TProvided]: {
|
|
19
|
-
[K in keyof TContract]: GuardInputOf<TContract[K], G>;
|
|
20
|
-
}[keyof TContract];
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Supplies guardInputs for every call from one place (the organization the
|
|
24
|
-
* UI is on, a device token), keyed by guard name; per-call guardInputs
|
|
25
|
-
* merge on top. Name the guards it covers in the caller's second type
|
|
26
|
-
* parameter, `new LambderCaller<Contract, "orgPermission">`, and calls to
|
|
27
|
-
* APIs whose guardInput guards are all covered no longer require the
|
|
28
|
-
* options argument. May be async; a throw fails the call as an unknown
|
|
29
|
-
* error before anything is sent.
|
|
30
|
-
*/
|
|
31
|
-
export type LambderGuardInputsProvider<TContract, TProvided extends string> = (apiName: keyof TContract & string) => LambderProvidedGuardInputs<TContract, TProvided> | Promise<LambderProvidedGuardInputs<TContract, TProvided>>;
|
|
32
|
-
/** Optional until the caller names provided guards: naming them without a provider would send nothing. */
|
|
33
|
-
type GuardInputsProviderOption<TContract, TProvided extends string> = [
|
|
34
|
-
TProvided
|
|
35
|
-
] extends [never] ? {
|
|
36
|
-
guardInputsProvider?: LambderGuardInputsProvider<TContract, TProvided>;
|
|
37
|
-
} : {
|
|
38
|
-
guardInputsProvider: LambderGuardInputsProvider<TContract, TProvided>;
|
|
39
|
-
};
|
|
40
|
-
/** An API's guardInput guards the provider does not cover: those the call must still pass. */
|
|
41
|
-
type RemainingGuardInputs<TEntry, TProvided extends string> = Omit<GuardInputsOf<TEntry>, TProvided>;
|
|
42
|
-
/**
|
|
43
|
-
* The options argument: optional normally, REQUIRED (with guardInputs) when
|
|
44
|
-
* the API's contract declares guardInput-mode guards the provider does not
|
|
45
|
-
* cover, so forgetting to send a guard's value is a compile error at the
|
|
46
|
-
* call site. Provided guards may still be overridden per call.
|
|
47
|
-
*/
|
|
48
|
-
type CallOptionsArg<TContract, TApiName, TProvided extends string> = IsAny<TContract> extends true ? [options?: LambderCallOptions] : TApiName extends keyof TContract ? [GuardInputsOf<TContract[TApiName]>] extends [never] ? [options?: LambderCallOptions] : [keyof RemainingGuardInputs<TContract[TApiName], TProvided>] extends [never] ? [options?: LambderCallOptions & {
|
|
49
|
-
guardInputs?: Partial<GuardInputsOf<TContract[TApiName]>>;
|
|
50
|
-
}] : [
|
|
51
|
-
options: LambderCallOptions & {
|
|
52
|
-
guardInputs: RemainingGuardInputs<TContract[TApiName], TProvided> & Partial<GuardInputsOf<TContract[TApiName]>>;
|
|
53
|
-
}
|
|
54
|
-
] : [options?: LambderCallOptions];
|
|
3
|
+
import { type LambderApiOutcome, type LambderValidationError } from '../shared/LambderApiOutcome.js';
|
|
4
|
+
import { type LambderCallOptionsArg, type LambderGuardInputsProviderOption } from '../shared/LambderCallOptions.js';
|
|
5
|
+
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError } from '../shared/LambderApiOutcome.js';
|
|
6
|
+
export type { LambderProvidedGuardInputs, LambderGuardInputsProvider } from '../shared/LambderCallOptions.js';
|
|
55
7
|
type VoidFunction = () => void | Promise<void>;
|
|
56
8
|
type FetchTracker = {
|
|
57
9
|
apiName: string;
|
|
@@ -73,7 +25,7 @@ type FetchEndEventHandler = (params: {
|
|
|
73
25
|
activeFetchList: FetchTracker[];
|
|
74
26
|
}) => void | Promise<void>;
|
|
75
27
|
type ErrorHandler = (err: Error) => void | Promise<void>;
|
|
76
|
-
type ValidationErrorHandler = (zodError:
|
|
28
|
+
type ValidationErrorHandler = (zodError: LambderValidationError) => (void | false) | Promise<(void | false)>;
|
|
77
29
|
type MessageHandler = (message: any) => void | Promise<void>;
|
|
78
30
|
/** One logical operation's rotating idempotency key: see LambderCaller.createIdempotencyKeyScope(). */
|
|
79
31
|
export type LambderIdempotencyKeyScope = {
|
|
@@ -82,32 +34,6 @@ export type LambderIdempotencyKeyScope = {
|
|
|
82
34
|
/** Call after a confirmed success: the next operation is a new intent. Returns the new key. */
|
|
83
35
|
rotate(): string;
|
|
84
36
|
};
|
|
85
|
-
export type LambderApiFailureReason = 'network' | 'timeout' | 'server' | 'validation' | 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage' | 'unknown';
|
|
86
|
-
/**
|
|
87
|
-
* Discriminated result of an API call: `ok: true` carries the payload, every
|
|
88
|
-
* failure carries a machine-readable reason, so "the server returned null"
|
|
89
|
-
* and "the request failed" are never conflated.
|
|
90
|
-
*/
|
|
91
|
-
export type LambderApiOutcome<T> = {
|
|
92
|
-
ok: true;
|
|
93
|
-
payload: T | null | undefined;
|
|
94
|
-
response: LambderApiResponse<T>;
|
|
95
|
-
} | {
|
|
96
|
-
ok: false;
|
|
97
|
-
reason: LambderApiFailureReason;
|
|
98
|
-
/** HTTP status, when a response was received. */
|
|
99
|
-
status?: number;
|
|
100
|
-
/** Envelope errorMessage, when the server provided one. */
|
|
101
|
-
errorMessage?: any;
|
|
102
|
-
/** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it). */
|
|
103
|
-
retryAfterSeconds?: number;
|
|
104
|
-
/** Underlying Error for network/timeout/server/unknown failures. */
|
|
105
|
-
error?: Error;
|
|
106
|
-
/** Zod issue detail for 'validation'. */
|
|
107
|
-
zodError?: z.ZodError;
|
|
108
|
-
/** The parsed envelope, when one was received (protocol-level failures). */
|
|
109
|
-
response?: LambderApiResponse<T>;
|
|
110
|
-
};
|
|
111
37
|
/** Per-call options: request extras plus overrides for every constructor handler. */
|
|
112
38
|
export type LambderCallOptions = {
|
|
113
39
|
headers?: Record<string, any>;
|
|
@@ -179,7 +105,7 @@ type LambderCallerBaseOptions = {
|
|
|
179
105
|
requestCompression?: LambderRequestCompressionOption;
|
|
180
106
|
};
|
|
181
107
|
/** Constructor options: the base options plus guardInputsProvider, mandatory once TProvided names guards. */
|
|
182
|
-
export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions &
|
|
108
|
+
export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions & LambderGuardInputsProviderOption<TContract, TProvided>;
|
|
183
109
|
/**
|
|
184
110
|
* @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
|
|
185
111
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
@@ -238,8 +164,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any, TPr
|
|
|
238
164
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
239
165
|
* instead of collapsing every failure to null. Never throws.
|
|
240
166
|
*/
|
|
241
|
-
apiOutcome<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest:
|
|
167
|
+
apiOutcome<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: LambderCallOptionsArg<TContract, TApiName, TProvidedGuards, LambderCallOptions>): Promise<LambderApiOutcome<TOutput>>;
|
|
242
168
|
/** Payload on success, null/undefined otherwise (indistinguishable from a null payload; prefer apiOutcome() when that matters). */
|
|
243
|
-
api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest:
|
|
169
|
+
api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: LambderCallOptionsArg<TContract, TApiName, TProvidedGuards, LambderCallOptions>): Promise<TOutput | null | undefined>;
|
|
244
170
|
}
|
|
245
|
-
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Cookies from 'js-cookie';
|
|
2
|
-
import {
|
|
2
|
+
import { compressPayloadGzip, isRequestCompressionAvailable, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from '../shared/LambderRequestPayload.js';
|
|
3
3
|
import { resolveCompressionOption } from '../shared/LambderCompressionOption.js';
|
|
4
|
+
import { resolveApiOutcome } from '../shared/LambderApiOutcome.js';
|
|
5
|
+
import { mergeGuardInputs, } from '../shared/LambderCallOptions.js';
|
|
4
6
|
/**
|
|
5
7
|
* @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
|
|
6
8
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
@@ -180,9 +182,7 @@ export default class LambderCaller {
|
|
|
180
182
|
const providedGuardInputs = this.guardInputsProvider
|
|
181
183
|
? await this.guardInputsProvider(apiName)
|
|
182
184
|
: undefined;
|
|
183
|
-
const guardInputs = providedGuardInputs
|
|
184
|
-
? { ...providedGuardInputs, ...options?.guardInputs }
|
|
185
|
-
: undefined;
|
|
185
|
+
const guardInputs = mergeGuardInputs(providedGuardInputs, options?.guardInputs);
|
|
186
186
|
// Compressed when enabled and the payload's JSON reaches the
|
|
187
187
|
// threshold; `compressRequest` overrides both ways, and a runtime
|
|
188
188
|
// without CompressionStream always sends the payload plainly.
|
|
@@ -192,7 +192,7 @@ export default class LambderCaller {
|
|
|
192
192
|
: options?.compressRequest === false ? null
|
|
193
193
|
: this.requestCompression?.minBytes ?? null;
|
|
194
194
|
const compressedPayload = compressionMinBytes !== null && payload !== undefined && isRequestCompressionAvailable()
|
|
195
|
-
? await
|
|
195
|
+
? await compressPayloadGzip(JSON.stringify(payload), compressionMinBytes)
|
|
196
196
|
: null;
|
|
197
197
|
let res;
|
|
198
198
|
try {
|
|
@@ -218,79 +218,47 @@ export default class LambderCaller {
|
|
|
218
218
|
await reportError(wrappedError);
|
|
219
219
|
return { ok: false, reason: timedOut ? 'timeout' : 'network', error: wrappedError };
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await fetchEnded(wrappedError);
|
|
235
|
-
await reportError(wrappedError);
|
|
236
|
-
return { ok: false, reason: 'server', status: res.status, errorMessage, error: wrappedError };
|
|
221
|
+
// The reading of the answer is shared with LambderInvokeCaller;
|
|
222
|
+
// only what to do about each outcome is this caller's.
|
|
223
|
+
const outcome = await resolveApiOutcome({
|
|
224
|
+
status: res.status,
|
|
225
|
+
statusText: res.statusText,
|
|
226
|
+
header: (name) => res.headers?.get?.(name) ?? null,
|
|
227
|
+
json: () => res.json(),
|
|
228
|
+
text: () => res.text(),
|
|
229
|
+
});
|
|
230
|
+
if (!outcome.ok && outcome.reason === 'server') {
|
|
231
|
+
await fetchEnded(outcome.error);
|
|
232
|
+
await reportError(outcome.error);
|
|
233
|
+
return outcome;
|
|
237
234
|
}
|
|
238
|
-
if (
|
|
239
|
-
// A 422 without Lambder's validation body (e.g. a proxy's
|
|
240
|
-
// error page) is a server failure, not a validation result.
|
|
241
|
-
let zodError;
|
|
242
|
-
try {
|
|
243
|
-
zodError = (await res.json())?.zodError;
|
|
244
|
-
}
|
|
245
|
-
catch { /* not JSON */ }
|
|
246
|
-
if (zodError === undefined) {
|
|
247
|
-
const wrappedError = new Error("Request failed: 422 without a validation body");
|
|
248
|
-
await fetchEnded(wrappedError);
|
|
249
|
-
await reportError(wrappedError);
|
|
250
|
-
return { ok: false, reason: 'server', status: res.status, error: wrappedError };
|
|
251
|
-
}
|
|
235
|
+
if (!outcome.ok && outcome.reason === 'validation') {
|
|
252
236
|
await fetchEnded(null);
|
|
253
237
|
if (apiInputValidationErrorHandler) {
|
|
254
|
-
await apiInputValidationErrorHandler(zodError);
|
|
238
|
+
await apiInputValidationErrorHandler(outcome.zodError);
|
|
255
239
|
}
|
|
256
240
|
else {
|
|
257
|
-
await reportError(new Error("API Input Validation Error", { cause: zodError }));
|
|
241
|
+
await reportError(new Error("API Input Validation Error", { cause: outcome.zodError }));
|
|
258
242
|
}
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
// Retry-After (delta-seconds) rides every refusal that knows its
|
|
262
|
-
// reset time, e.g. a rate limit; absent or unreadable is undefined.
|
|
263
|
-
const retryAfterValue = Number(res.headers.get("retry-after") ?? NaN);
|
|
264
|
-
const retryAfter = Number.isFinite(retryAfterValue) && retryAfterValue >= 0 ? { retryAfterSeconds: retryAfterValue } : {};
|
|
265
|
-
let data;
|
|
266
|
-
try {
|
|
267
|
-
data = await res.json();
|
|
268
|
-
if (data === null || typeof data !== "object")
|
|
269
|
-
throw new Error("Response is not an object");
|
|
270
|
-
}
|
|
271
|
-
catch (err) {
|
|
272
|
-
// A non-envelope body (e.g. an HTML error page) is a server failure.
|
|
273
|
-
const wrappedError = new Error("Request failed: response is not a valid API envelope (status " + res.status + ")", { cause: err });
|
|
274
|
-
await fetchEnded(wrappedError);
|
|
275
|
-
await reportError(wrappedError);
|
|
276
|
-
return { ok: false, reason: 'server', status: res.status, error: wrappedError };
|
|
243
|
+
return outcome;
|
|
277
244
|
}
|
|
245
|
+
const data = outcome.response;
|
|
278
246
|
await fetchEnded(data);
|
|
279
247
|
if (data.logList?.length) {
|
|
280
248
|
for (const record of data.logList) {
|
|
281
249
|
console.log("[lambder]", record);
|
|
282
250
|
}
|
|
283
251
|
}
|
|
284
|
-
if (
|
|
252
|
+
if (!outcome.ok && outcome.reason === 'versionExpired') {
|
|
285
253
|
if (versionExpiredHandler) {
|
|
286
254
|
await versionExpiredHandler();
|
|
287
255
|
}
|
|
288
256
|
else {
|
|
289
257
|
await reportError(new Error("Version Expired; Please refresh;"));
|
|
290
258
|
}
|
|
291
|
-
return
|
|
259
|
+
return outcome;
|
|
292
260
|
}
|
|
293
|
-
if (
|
|
261
|
+
if (!outcome.ok && outcome.reason === 'sessionExpired') {
|
|
294
262
|
this.clearSessionCookies();
|
|
295
263
|
if (sessionExpiredHandler) {
|
|
296
264
|
await sessionExpiredHandler();
|
|
@@ -298,27 +266,27 @@ export default class LambderCaller {
|
|
|
298
266
|
else {
|
|
299
267
|
await reportError(new Error("Session Expired; Please log in again;"));
|
|
300
268
|
}
|
|
301
|
-
return
|
|
269
|
+
return outcome;
|
|
302
270
|
}
|
|
303
|
-
if (
|
|
271
|
+
if (!outcome.ok && outcome.reason === 'notAuthorized') {
|
|
304
272
|
if (notAuthorizedHandler) {
|
|
305
273
|
await notAuthorizedHandler();
|
|
306
274
|
}
|
|
307
275
|
else {
|
|
308
276
|
await reportError(new Error("Not Authorized;"));
|
|
309
277
|
}
|
|
310
|
-
return
|
|
278
|
+
return outcome;
|
|
311
279
|
}
|
|
312
280
|
if (data.message && messageHandler) {
|
|
313
281
|
await messageHandler(data.message);
|
|
314
282
|
}
|
|
315
|
-
if (
|
|
283
|
+
if (!outcome.ok && outcome.reason === 'errorMessage') {
|
|
316
284
|
if (errorMessageHandler) {
|
|
317
285
|
await errorMessageHandler(data.errorMessage);
|
|
318
286
|
}
|
|
319
|
-
return
|
|
287
|
+
return outcome;
|
|
320
288
|
}
|
|
321
|
-
return
|
|
289
|
+
return outcome;
|
|
322
290
|
}
|
|
323
291
|
catch (err) {
|
|
324
292
|
// Escape hatch for anything above (typically an app handler throwing):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, decompressPayloadGzip } from '../shared/LambderRequestPayload.js';
|
|
2
2
|
export default class LambderMSW {
|
|
3
3
|
apiPath;
|
|
4
4
|
apiVersion;
|
|
@@ -46,15 +46,15 @@ export default class LambderMSW {
|
|
|
46
46
|
// A caller with requestCompression on sends the payload gzipped;
|
|
47
47
|
// mock handlers still receive the payload itself, and the wire
|
|
48
48
|
// fields are consumed the way the server consumes them.
|
|
49
|
-
if (typeof body[
|
|
49
|
+
if (typeof body[COMPRESSED_PAYLOAD_GZ_FIELD] === 'string') {
|
|
50
50
|
try {
|
|
51
|
-
body.payload = await
|
|
51
|
+
body.payload = await decompressPayloadGzip(body[COMPRESSED_PAYLOAD_GZ_FIELD]);
|
|
52
52
|
}
|
|
53
53
|
catch {
|
|
54
54
|
console.warn("LambderMSW: Failed to decompress the request payload");
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
delete body[
|
|
57
|
+
delete body[COMPRESSED_PAYLOAD_GZ_FIELD];
|
|
58
58
|
delete body[COMPRESSED_PAYLOAD_BYTES_FIELD];
|
|
59
59
|
}
|
|
60
60
|
try {
|
package/dist/client.d.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* the root entry (`"lambder"`) is the server surface.
|
|
8
8
|
*/
|
|
9
9
|
export { default as LambderCaller } from "./client/LambderCaller.js";
|
|
10
|
-
export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope, } from "./client/LambderCaller.js";
|
|
10
|
+
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope, } from "./client/LambderCaller.js";
|
|
11
11
|
export { LambderApiError, isLambderApiError, refuse, LAMBDER_REFUSAL_CODES } from "./shared/LambderApiError.js";
|
|
12
12
|
export type { LambderApiErrorOptions, LambderRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/LambderApiError.js";
|
|
13
13
|
export type { ApiContractShape, LambderApiResponse, LambderApiResponseConfig } from "./shared/LambderApiContract.js";
|
|
14
|
-
export {
|
|
15
|
-
export type {
|
|
14
|
+
export { describeCrash, errorFromCrashDetail } from "./shared/LambderCrashDetail.js";
|
|
15
|
+
export type { LambderCrashDetail, LambderCrashCause } from "./shared/LambderCrashDetail.js";
|
|
16
|
+
export { compressPayloadGzip, decompressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/LambderRequestPayload.js";
|
|
17
|
+
export type { LambderCompressedGzipPayload, LambderCompressedBrotliPayload, LambderRequestCompressionOption, LambderRequestCompressionSettings, } from "./shared/LambderRequestPayload.js";
|
|
16
18
|
export { resolveCompressionOption } from "./shared/LambderCompressionOption.js";
|
|
17
19
|
export type { LambderCompressionOption, LambderCompressionSettingsBase } from "./shared/LambderCompressionOption.js";
|
|
18
20
|
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
|
package/dist/client.js
CHANGED
|
@@ -11,8 +11,10 @@ export { default as LambderCaller } from "./client/LambderCaller.js";
|
|
|
11
11
|
// Typed API refusals (isomorphic: shared code may throw them from anywhere;
|
|
12
12
|
// in the browser they are plain Errors).
|
|
13
13
|
export { LambderApiError, isLambderApiError, refuse, LAMBDER_REFUSAL_CODES } from "./shared/LambderApiError.js";
|
|
14
|
+
// A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
|
|
15
|
+
export { describeCrash, errorFromCrashDetail } from "./shared/LambderCrashDetail.js";
|
|
14
16
|
// Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
|
|
15
|
-
export {
|
|
17
|
+
export { compressPayloadGzip, decompressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/LambderRequestPayload.js";
|
|
16
18
|
// The compression option vocabulary every Lambder surface shares (pure: no zlib).
|
|
17
19
|
export { resolveCompressionOption } from "./shared/LambderCompressionOption.js";
|
|
18
20
|
// Type-safe templating (tagged templates with auto-escaping)
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -297,7 +297,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
297
297
|
addSessionRoute<TPath extends Path>(condition: TPath, actionFn: (ctx: LambderSessionRenderContext<any, TSessionData, PathParamsOf<TPath>>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
298
298
|
addSessionRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: SessionActionFunction<TSessionData>): this;
|
|
299
299
|
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract, any, any, any, any, any>) => Lambder<TSessionData, _TNewContract, any, any, any, any, any>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract), _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired>;
|
|
300
|
-
addApi<TName extends string, TInput extends z.
|
|
300
|
+
addApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.infer<TInput>, false> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.infer<TInput>, false> = never>(name: TName, schema: {
|
|
301
301
|
input: TInput;
|
|
302
302
|
output: TOutput;
|
|
303
303
|
} & {
|
|
@@ -308,7 +308,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
308
308
|
ttlSeconds?: number;
|
|
309
309
|
}) : never;
|
|
310
310
|
} & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt>, handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>, LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired>;
|
|
311
|
-
addSessionApi<TName extends string, TInput extends z.
|
|
311
|
+
addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, z.infer<TInput>, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, z.infer<TInput>, true> = never>(name: TName, schema: {
|
|
312
312
|
input: TInput;
|
|
313
313
|
output: TOutput;
|
|
314
314
|
} & {
|
package/dist/core/Lambder.js
CHANGED
|
@@ -11,7 +11,7 @@ import { LambderFiles } from "./LambderFiles.js";
|
|
|
11
11
|
import { isLambderApiError, LAMBDER_REFUSAL_CODES } from "../shared/LambderApiError.js";
|
|
12
12
|
import { LambderApiPolicyEngine } from "../policies/LambderApiPolicies.js";
|
|
13
13
|
import { createContext, isV2HttpEvent, restoreCompressedApiPayload } from "./LambderContext.js";
|
|
14
|
-
import {
|
|
14
|
+
import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/LambderRequestPayload.js";
|
|
15
15
|
/**
|
|
16
16
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
17
17
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
@@ -85,7 +85,7 @@ export default class Lambder {
|
|
|
85
85
|
etag: options.etag ?? DEFAULT_FINALIZE_OPTIONS.etag,
|
|
86
86
|
maxResponseBytes: options.maxResponseBytes ?? DEFAULT_FINALIZE_OPTIONS.maxResponseBytes,
|
|
87
87
|
};
|
|
88
|
-
this.maxRequestPayloadBytes = options.maxRequestPayloadBytes ??
|
|
88
|
+
this.maxRequestPayloadBytes = options.maxRequestPayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES;
|
|
89
89
|
if (!Number.isSafeInteger(this.maxRequestPayloadBytes) || this.maxRequestPayloadBytes <= 0) {
|
|
90
90
|
throw new Error("maxRequestPayloadBytes must be a positive integer");
|
|
91
91
|
}
|
|
@@ -211,7 +211,14 @@ export default class Lambder {
|
|
|
211
211
|
async inputValidationRefusal(ctx, resolver, zodError) {
|
|
212
212
|
if (this.apiInputValidationErrorHandler)
|
|
213
213
|
return await this.apiInputValidationErrorHandler(ctx, resolver, zodError);
|
|
214
|
-
|
|
214
|
+
// Spelled out rather than serialized as-is: zod 4 keeps `issues` as a
|
|
215
|
+
// non-enumerable property, so JSON.stringify(zodError) would carry the
|
|
216
|
+
// issues only inside the message string, and a client's validation
|
|
217
|
+
// handler would receive a ZodError with nothing to branch on.
|
|
218
|
+
return resolver.json({
|
|
219
|
+
error: "Input validation failed",
|
|
220
|
+
zodError: { name: zodError.name, message: zodError.message, issues: zodError.issues },
|
|
221
|
+
}, { statusCode: 422 });
|
|
215
222
|
}
|
|
216
223
|
/** Registration-time checks shared by addApi/addSessionApi. */
|
|
217
224
|
assertApiRegistration(name, mode, options) {
|
|
@@ -66,11 +66,12 @@ export type LambderRestorePayloadResult = {
|
|
|
66
66
|
message: string;
|
|
67
67
|
};
|
|
68
68
|
/**
|
|
69
|
-
* Restores a request payload the caller sent
|
|
70
|
-
* `payloadBytes`) onto ctx.post.payload and
|
|
71
|
-
* stage (rate-limit key slices, guards, input
|
|
72
|
-
* an ordinary payload and needs no awareness
|
|
73
|
-
*
|
|
69
|
+
* Restores a request payload the caller sent compressed (`payloadGz` or
|
|
70
|
+
* `payloadBr`, beside `payloadBytes`) onto ctx.post.payload and
|
|
71
|
+
* ctx.apiPayload, so every later stage (rate-limit key slices, guards, input
|
|
72
|
+
* validation, the handler) reads an ordinary payload and needs no awareness
|
|
73
|
+
* of the wire format. The field names the encoding; a request carrying both
|
|
74
|
+
* is refused. A request that sent a plain payload passes through untouched.
|
|
74
75
|
*
|
|
75
76
|
* Every failure answers with a message instead of throwing: a malformed body
|
|
76
77
|
* is a client error, not a crash. The declared byte length both bounds the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cookieParser from "cookie";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, } from "../shared/LambderRequestPayload.js";
|
|
3
|
+
import { restoreText, LambderCompressionError, LAMBDER_RESTORE_FAILURES, } from "../shared/LambderCompressionCodec.js";
|
|
4
4
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
5
5
|
export const isV2HttpEvent = (event) => !!event && typeof event === "object"
|
|
6
6
|
&& event.version === "2.0"
|
|
@@ -101,11 +101,12 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
101
101
|
};
|
|
102
102
|
};
|
|
103
103
|
/**
|
|
104
|
-
* Restores a request payload the caller sent
|
|
105
|
-
* `payloadBytes`) onto ctx.post.payload and
|
|
106
|
-
* stage (rate-limit key slices, guards, input
|
|
107
|
-
* an ordinary payload and needs no awareness
|
|
108
|
-
*
|
|
104
|
+
* Restores a request payload the caller sent compressed (`payloadGz` or
|
|
105
|
+
* `payloadBr`, beside `payloadBytes`) onto ctx.post.payload and
|
|
106
|
+
* ctx.apiPayload, so every later stage (rate-limit key slices, guards, input
|
|
107
|
+
* validation, the handler) reads an ordinary payload and needs no awareness
|
|
108
|
+
* of the wire format. The field names the encoding; a request carrying both
|
|
109
|
+
* is refused. A request that sent a plain payload passes through untouched.
|
|
109
110
|
*
|
|
110
111
|
* Every failure answers with a message instead of throwing: a malformed body
|
|
111
112
|
* is a client error, not a crash. The declared byte length both bounds the
|
|
@@ -114,11 +115,18 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
114
115
|
*/
|
|
115
116
|
export const restoreCompressedApiPayload = async (ctx, maxPayloadBytes) => {
|
|
116
117
|
const post = ctx.post;
|
|
117
|
-
const
|
|
118
|
-
|
|
118
|
+
const hasGzip = post[COMPRESSED_PAYLOAD_GZ_FIELD] !== undefined;
|
|
119
|
+
const hasBrotli = post[COMPRESSED_PAYLOAD_BR_FIELD] !== undefined;
|
|
120
|
+
if (!hasGzip && !hasBrotli)
|
|
119
121
|
return { ok: true };
|
|
122
|
+
if (hasGzip && hasBrotli) {
|
|
123
|
+
return { ok: false, message: `Request carries both ${COMPRESSED_PAYLOAD_GZ_FIELD} and ${COMPRESSED_PAYLOAD_BR_FIELD}; send one.` };
|
|
124
|
+
}
|
|
125
|
+
const field = hasGzip ? COMPRESSED_PAYLOAD_GZ_FIELD : COMPRESSED_PAYLOAD_BR_FIELD;
|
|
126
|
+
const encoding = hasGzip ? "gzip" : "br";
|
|
127
|
+
const compressed = post[field];
|
|
120
128
|
if (typeof compressed !== "string") {
|
|
121
|
-
return { ok: false, message: `Request ${
|
|
129
|
+
return { ok: false, message: `Request ${field} must be a base64 string.` };
|
|
122
130
|
}
|
|
123
131
|
const declaredBytes = post[COMPRESSED_PAYLOAD_BYTES_FIELD];
|
|
124
132
|
if (typeof declaredBytes !== "number" || !Number.isSafeInteger(declaredBytes) || declaredBytes <= 0) {
|
|
@@ -131,7 +139,7 @@ export const restoreCompressedApiPayload = async (ctx, maxPayloadBytes) => {
|
|
|
131
139
|
// ones a stored record gets; only the wording of the refusal is ours.
|
|
132
140
|
let json;
|
|
133
141
|
try {
|
|
134
|
-
json = await
|
|
142
|
+
json = await restoreText(Buffer.from(compressed, "base64"), encoding, { declaredBytes });
|
|
135
143
|
}
|
|
136
144
|
catch (err) {
|
|
137
145
|
const reason = err instanceof LambderCompressionError ? err.reason : null;
|
|
@@ -146,7 +154,7 @@ export const restoreCompressedApiPayload = async (ctx, maxPayloadBytes) => {
|
|
|
146
154
|
catch {
|
|
147
155
|
return { ok: false, message: "Compressed request payload is not valid JSON." };
|
|
148
156
|
}
|
|
149
|
-
delete post[
|
|
157
|
+
delete post[field];
|
|
150
158
|
delete post[COMPRESSED_PAYLOAD_BYTES_FIELD];
|
|
151
159
|
post.payload = payload;
|
|
152
160
|
ctx.apiPayload = payload;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import LambderResponseBuilder, { type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
|
|
1
|
+
import LambderResponseBuilder, { type LambderApiAnswer, type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
|
|
2
2
|
import type { LambderResponse } from "./LambderResponse.js";
|
|
3
3
|
type SyncDie<T extends (...args: any[]) => LambderResponse> = (...args: Parameters<T>) => never;
|
|
4
4
|
type AsyncDie<T extends (...args: any[]) => Promise<LambderResponse>> = (...args: Parameters<T>) => Promise<never>;
|
|
@@ -13,8 +13,8 @@ export interface DieResolverMethods<TOutput> {
|
|
|
13
13
|
redirect: SyncDie<LambderResponseBuilder["redirect"]>;
|
|
14
14
|
versionExpired: SyncDie<LambderResponseBuilder["versionExpired"]>;
|
|
15
15
|
fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
|
|
16
|
-
api:
|
|
17
|
-
apiBinary:
|
|
16
|
+
api: LambderApiAnswer<TOutput, never>;
|
|
17
|
+
apiBinary: LambderApiAnswer<TOutput, never>;
|
|
18
18
|
file: AsyncDie<LambderResponseBuilder["file"]>;
|
|
19
19
|
templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
|
|
20
20
|
}
|
|
@@ -29,7 +29,9 @@ export interface DieResolverMethods<TOutput> {
|
|
|
29
29
|
export default class LambderResolver<TOutput = any> extends LambderResponseBuilder<TOutput> {
|
|
30
30
|
die: DieResolverMethods<TOutput>;
|
|
31
31
|
constructor(...args: ConstructorParameters<typeof LambderResponseBuilder>);
|
|
32
|
-
api(payload: TOutput
|
|
33
|
-
|
|
32
|
+
api(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
33
|
+
api(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
34
|
+
apiBinary(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
35
|
+
apiBinary(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
34
36
|
}
|
|
35
37
|
export {};
|
|
@@ -22,13 +22,17 @@ 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
|
-
|
|
25
|
+
// Overloaded on the payload (see LambderApiAnswer); the implementation takes both shapes.
|
|
26
|
+
api: ((payload, config, options) => {
|
|
27
|
+
throw this.api(payload, config, options);
|
|
28
|
+
}),
|
|
29
|
+
apiBinary: ((payload, config, options) => {
|
|
30
|
+
throw this.apiBinary(payload, config, options);
|
|
31
|
+
}),
|
|
27
32
|
file: async (...a) => { throw await this.file(...a); },
|
|
28
33
|
templateFile: async (...a) => { throw await this.templateFile(...a); },
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
|
-
// Override api method with proper output typing
|
|
32
36
|
api(payload, config, options) {
|
|
33
37
|
return super.api(payload, config, options);
|
|
34
38
|
}
|
|
@@ -16,6 +16,18 @@ export type LambderResponseOptions = {
|
|
|
16
16
|
/** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
|
|
17
17
|
etag?: boolean | "auto";
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* The two shapes of an API answer: the output the contract declares, or
|
|
21
|
+
* `null` beside a config that says why (a refusal flag, an `errorMessage`, a
|
|
22
|
+
* `message`). A bare `res.api(null)` compiles only when the output type
|
|
23
|
+
* itself allows null, so a success payload is always the declared output,
|
|
24
|
+
* which is what lets a typed caller (LambderInvokeCaller.api) promise it.
|
|
25
|
+
* Untyped resolvers (`TOutput = any`) accept anything, as before.
|
|
26
|
+
*/
|
|
27
|
+
export type LambderApiAnswer<TOutput, TResult> = {
|
|
28
|
+
(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): TResult;
|
|
29
|
+
(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): TResult;
|
|
30
|
+
};
|
|
19
31
|
export type LambderRawResponseInit = {
|
|
20
32
|
statusCode: HttpStatusCode;
|
|
21
33
|
headers?: LambderHeadersInput;
|
|
@@ -76,7 +88,9 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
76
88
|
templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
|
|
77
89
|
htmlVirtualSlots?: boolean;
|
|
78
90
|
}): Promise<LambderResponse>;
|
|
79
|
-
api(payload: TResponse
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
api(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
92
|
+
api(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
93
|
+
/** Same as api() but forces compression of the response body. */
|
|
94
|
+
apiBinary(payload: TResponse, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
95
|
+
apiBinary(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
82
96
|
}
|