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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { classifyDeliveryFailure, describeFailure, errorFromFunctionError, LambderInvokeError, parseFunctionError, } from "./LambderInvokeOutcome.js";
|
|
24
24
|
import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
|
|
25
|
-
import { readApiSignature } from "../shared/wire/
|
|
25
|
+
import { readApiSignature } from "../shared/wire/LambderApiSignatureMap.js";
|
|
26
26
|
import { resolveApiOutcome } from "../shared/wire/LambderApiOutcome.js";
|
|
27
27
|
import { mergeGuardInputs, } from "../shared/wire/LambderCallOptions.js";
|
|
28
28
|
import { beginIdempotentAttempt, IDEMPOTENT_ATTEMPT_NOT_SENT } from "../shared/wire/LambderIdempotencyKeyScope.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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
import type { LambderApiSignatureMap } from "../shared/wire/
|
|
2
|
+
import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
|
|
3
3
|
import type { LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
4
4
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
5
5
|
import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
@@ -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.
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Contracts are built via method chaining and inferred using typeof lambder.ApiContract
|
|
5
5
|
*/
|
|
6
6
|
import type { LambderCrashDetail } from "./LambderCrashDetail.js";
|
|
7
|
-
import type { LambderNonEmptyOptionMap } from "../util/LambderTypeUtilities.js";
|
|
8
7
|
import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
|
|
9
8
|
import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "./LambderApiOptionValues.js";
|
|
10
9
|
/** Whether an endpoint runs without a session or requires one (addApi versus addSessionApi). */
|
|
@@ -32,15 +31,18 @@ export type LambderApiContractShape = Record<string, {
|
|
|
32
31
|
/** Present when the API declares idempotency: the `idempotency` option exactly as written. */
|
|
33
32
|
idempotency?: LambderApiIdempotencyOption;
|
|
34
33
|
}>;
|
|
35
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Envelope flags and channels beside (or instead of) the payload: what a
|
|
36
|
+
* refusal, the pipeline's own answers, a hook or an error handler set. An API
|
|
37
|
+
* handler sets none of them itself: it returns its output or refuses.
|
|
38
|
+
*/
|
|
36
39
|
export type LambderApiResponseConfig = {
|
|
37
40
|
versionExpired?: boolean;
|
|
38
41
|
sessionExpired?: boolean;
|
|
39
42
|
notAuthorized?: boolean;
|
|
40
|
-
message?: any;
|
|
41
43
|
/**
|
|
42
|
-
* A refusal message, or a plain string when writing
|
|
43
|
-
* (`res.api(null, { errorMessage
|
|
44
|
+
* A refusal message, or a plain string when writing (a refusal thrown
|
|
45
|
+
* with refuse(), or an error handler's `res.api(null, { errorMessage })`): the envelope goes out with
|
|
44
46
|
* the message object either way. Read off the wire it can still be a
|
|
45
47
|
* string, or no message at all, wherever a Lambder server did not write
|
|
46
48
|
* the body (a hand-built mock answer, a proxy); refusalMessageOf reads
|
|
@@ -58,15 +60,8 @@ export type LambderApiResponseConfig = {
|
|
|
58
60
|
crash?: LambderCrashDetail;
|
|
59
61
|
};
|
|
60
62
|
/**
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
* message reaches the caller as a success whose payload is null, which is
|
|
64
|
-
* indistinguishable from an endpoint that answered nothing on purpose.
|
|
65
|
-
*/
|
|
66
|
-
export type LambderApiNullAnswerConfig = LambderNonEmptyOptionMap<Pick<LambderApiResponseConfig, "versionExpired" | "sessionExpired" | "notAuthorized" | "errorMessage" | "message">> & LambderApiResponseConfig;
|
|
67
|
-
/**
|
|
68
|
-
* The API wire envelope both sides speak: res.api() emits it, LambderCaller
|
|
69
|
-
* parses it. `apiVersion` is always there (null when the server set none):
|
|
63
|
+
* The API wire envelope both sides speak: the server writes it around a
|
|
64
|
+
* handler's output or a refusal, LambderCaller parses it. `apiVersion` is always there (null when the server set none):
|
|
70
65
|
* it is how a reader tells a Lambder envelope from another JSON answer, such
|
|
71
66
|
* as API Gateway's own `{ "message": ... }` errors, so an answer without it
|
|
72
67
|
* reads as a server failure.
|
|
@@ -26,10 +26,9 @@ export type LambderApiRefusalOptions = {
|
|
|
26
26
|
/**
|
|
27
27
|
* A typed refusal: "this request is denied/invalid" as opposed to "the server
|
|
28
28
|
* crashed". Throw it from anywhere in an API call's call stack (handlers,
|
|
29
|
-
* hooks, or nested helpers
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* of routing it through setGlobalErrorHandler, so refusals never reach crash
|
|
29
|
+
* hooks, guards or nested helpers) and the render pipeline maps it onto the
|
|
30
|
+
* structured API envelope (its errorMessage, notAuthorized and
|
|
31
|
+
* sessionExpired) instead of routing it through setGlobalErrorHandler, so refusals never reach crash
|
|
33
32
|
* logging and clients receive a parseable response.
|
|
34
33
|
*
|
|
35
34
|
* Thrown outside an API call (e.g. in a route handler) it behaves like any
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A typed refusal: "this request is denied/invalid" as opposed to "the server
|
|
3
3
|
* crashed". Throw it from anywhere in an API call's call stack (handlers,
|
|
4
|
-
* hooks, or nested helpers
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* of routing it through setGlobalErrorHandler, so refusals never reach crash
|
|
4
|
+
* hooks, guards or nested helpers) and the render pipeline maps it onto the
|
|
5
|
+
* structured API envelope (its errorMessage, notAuthorized and
|
|
6
|
+
* sessionExpired) instead of routing it through setGlobalErrorHandler, so refusals never reach crash
|
|
8
7
|
* logging and clients receive a parseable response.
|
|
9
8
|
*
|
|
10
9
|
* Thrown outside an API call (e.g. in a route handler) it behaves like any
|
|
@@ -5,7 +5,7 @@ import { type LambderLambdaHttpResult } from "../invoke/LambderLambdaEvent.js";
|
|
|
5
5
|
import type { LambderCreatedSession } from "../session/LambderSessionManager.js";
|
|
6
6
|
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
7
7
|
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
8
|
-
import type { LambderApiSignatureMap } from "../shared/wire/
|
|
8
|
+
import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
|
|
9
9
|
import type { LambderGuardInputsProviderOption } from "../shared/wire/LambderCallOptions.js";
|
|
10
10
|
/**
|
|
11
11
|
* How one visitor differs from the next. Everything is optional: a visitor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.2",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|
|
File without changes
|
|
File without changes
|