lambder 6.0.2 → 7.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 +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
2
|
-
import type {
|
|
3
|
-
import type
|
|
2
|
+
import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
|
|
3
|
+
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
4
|
+
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
4
5
|
export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
6
|
+
/**
|
|
7
|
+
* Which API Gateway payload format an event arrived in, and the format its
|
|
8
|
+
* response leaves in. Declared here, beside the detection it comes from:
|
|
9
|
+
* LambderResponse holds the emitters that read it, and having the type there
|
|
10
|
+
* as well made the two core modules import each other.
|
|
11
|
+
*/
|
|
12
|
+
export type LambderHttpEventFormat = "v1" | "v2";
|
|
5
13
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
6
14
|
export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxyEventV2;
|
|
7
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Everything a route or API handler knows about the request. Extends the
|
|
17
|
+
* API core's call context (session, guardData, responseHeaders, logList),
|
|
18
|
+
* which is the part the pipeline and the session controller work on; the
|
|
19
|
+
* rest is the HTTP request as the Lambda event delivered it.
|
|
20
|
+
*/
|
|
21
|
+
export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TSessionData = any> = {
|
|
8
22
|
host: string;
|
|
9
23
|
path: string;
|
|
10
24
|
pathParams: TPathParams;
|
|
11
25
|
method: string;
|
|
12
26
|
get: Record<string, string | undefined>;
|
|
13
|
-
post: Record<string,
|
|
27
|
+
post: Record<string, unknown>;
|
|
14
28
|
/** Cookies by name (the first value when a name arrived more than once; see cookieList). */
|
|
15
29
|
cookie: Record<string, string>;
|
|
16
30
|
/**
|
|
@@ -21,8 +35,26 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
21
35
|
* browser's order says nothing about which copy is current.
|
|
22
36
|
*/
|
|
23
37
|
cookieList: Record<string, string[]>;
|
|
24
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The session, once something read or created one: the pipeline sets it on
|
|
40
|
+
* a session API, and getSessionController(ctx).createSession writes it
|
|
41
|
+
* here too. Null everywhere else, which is why a route or public API reads
|
|
42
|
+
* it as `ctx.session?.data`. Typing it as the literal `null` said the
|
|
43
|
+
* opposite of what the code does: after createSession the field was still
|
|
44
|
+
* `never`, and addSessionRoute needed a double cast to hand the handler
|
|
45
|
+
* the same object it already held.
|
|
46
|
+
*/
|
|
47
|
+
session: LambderSessionRecord<TSessionData> | null;
|
|
48
|
+
/**
|
|
49
|
+
* The API call this request is, as the core sees it, or null for a
|
|
50
|
+
* route. Carries the envelope's fields (name, version, CSRF token,
|
|
51
|
+
* payload, guard inputs, idempotency key) and the request's headers,
|
|
52
|
+
* cookies, ip and host.
|
|
53
|
+
*/
|
|
54
|
+
api: LambderApiRequest | null;
|
|
55
|
+
/** The API name, or null for a route (api.apiName). */
|
|
25
56
|
apiName: string | null;
|
|
57
|
+
/** The API payload: as posted until validation, the parsed value inside the handler. */
|
|
26
58
|
apiPayload: TApiPayload;
|
|
27
59
|
/**
|
|
28
60
|
* Outputs of this API's guards, keyed by guard name. Only guards the API
|
|
@@ -33,49 +65,27 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
33
65
|
headers: APIGatewayProxyEventHeaders;
|
|
34
66
|
/** Decoded request body, exactly as received (e.g. for webhook signature verification). */
|
|
35
67
|
rawBody: string;
|
|
36
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* The address the gateway observed, or the leftmost entry of the first
|
|
70
|
+
* header named in `trustedClientIpHeaders` that carries one; nothing is
|
|
71
|
+
* trusted by default. One spelling per address (port and brackets
|
|
72
|
+
* stripped, lowercased, length-bounded), so a `per: "ip"` limit keys one
|
|
73
|
+
* counter per client.
|
|
74
|
+
*/
|
|
37
75
|
ip: string;
|
|
38
76
|
/** Case-insensitive request header lookup. */
|
|
39
77
|
header: (name: string) => string | undefined;
|
|
40
78
|
event: LambderHttpEvent;
|
|
41
79
|
lambdaContext: Context;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
value: string | string[];
|
|
49
|
-
}[];
|
|
50
|
-
addHeaderFnAccumulator: {
|
|
51
|
-
key: string;
|
|
52
|
-
value: string;
|
|
53
|
-
}[];
|
|
54
|
-
logToApiResponseAccumulator: any[];
|
|
55
|
-
};
|
|
80
|
+
/** Which API Gateway payload format the event arrived in, and the response leaves in. */
|
|
81
|
+
eventFormat: LambderHttpEventFormat;
|
|
82
|
+
/** Response headers written during the request (res.setHeader, res.addHeader, session cookies), applied onto the response at the end. */
|
|
83
|
+
responseHeaders: LambderAnswerHeaders;
|
|
84
|
+
/** Entries for the API envelope's logList channel (res.logToApiResponse). */
|
|
85
|
+
logList: unknown[];
|
|
56
86
|
};
|
|
57
|
-
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData>, 'session'> & {
|
|
58
|
-
session:
|
|
87
|
+
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData, SessionData>, 'session'> & {
|
|
88
|
+
session: LambderSessionRecord<SessionData>;
|
|
59
89
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export type LambderRestorePayloadResult = {
|
|
63
|
-
ok: true;
|
|
64
|
-
} | {
|
|
65
|
-
ok: false;
|
|
66
|
-
message: string;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
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.
|
|
75
|
-
*
|
|
76
|
-
* Every failure answers with a message instead of throwing: a malformed body
|
|
77
|
-
* is a client error, not a crash. The declared byte length both bounds the
|
|
78
|
-
* decompression and verifies it, so an over-large or tampered body is
|
|
79
|
-
* refused rather than expanded.
|
|
80
|
-
*/
|
|
81
|
-
export declare const restoreCompressedApiPayload: (ctx: LambderRenderContext, maxPayloadBytes: number) => Promise<LambderRestorePayloadResult>;
|
|
90
|
+
/** The render context for one request: everything a route handler, an API handler, a hook or a guard reads about it, built once from the Lambda event. */
|
|
91
|
+
export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, apiPath: string, trustedClientIpHeaders?: readonly string[]) => LambderRenderContext;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
2
|
+
import { resolveClientIp } from "../shared/util/LambderClientIp.js";
|
|
3
|
+
import { base64ToText } from "../shared/util/LambderBase64.js";
|
|
4
|
+
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
4
5
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
5
6
|
export const isV2HttpEvent = (event) => !!event && typeof event === "object"
|
|
6
7
|
&& event.version === "2.0"
|
|
7
8
|
&& !!event.requestContext?.http;
|
|
8
|
-
|
|
9
|
+
/** The render context for one request: everything a route handler, an API handler, a hook or a guard reads about it, built once from the Lambda event. */
|
|
10
|
+
export const createContext = (event, lambdaContext, apiPath, trustedClientIpHeaders = []) => {
|
|
9
11
|
// Normalize the two API Gateway payload formats into one shape.
|
|
10
12
|
const eventFormat = isV2HttpEvent(event) ? "v2" : "v1";
|
|
11
13
|
let host;
|
|
@@ -37,126 +39,55 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
37
39
|
path = event.path;
|
|
38
40
|
method = event.httpMethod;
|
|
39
41
|
get = event.queryStringParameters || {};
|
|
40
|
-
|
|
42
|
+
// A REST API keeps only the LAST value of a repeated header in
|
|
43
|
+
// `headers` and every value in `multiValueHeaders`, and HTTP/2 lets a
|
|
44
|
+
// client split its cookies across several Cookie headers. The session
|
|
45
|
+
// layer weighs every copy of a cookie name, so dropping one is
|
|
46
|
+
// dropping a candidate session; v2's `event.cookies` already carries
|
|
47
|
+
// them all.
|
|
48
|
+
const cookieHeaders = event.multiValueHeaders?.Cookie ?? event.multiValueHeaders?.cookie;
|
|
49
|
+
cookiePairs = (cookieHeaders?.length ? cookieHeaders.join("; ") : (headers.Cookie || headers.cookie || "")).split(";");
|
|
41
50
|
sourceIp = event.requestContext?.identity?.sourceIp || "";
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (value !== undefined)
|
|
49
|
-
(cookieList[name] ??= []).push(value);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const cookie = Object.fromEntries(Object.entries(cookieList).map(([name, values]) => [name, values[0]]));
|
|
53
|
-
const lowercasedHeaders = {};
|
|
54
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
55
|
-
if (value !== undefined)
|
|
56
|
-
lowercasedHeaders[key.toLowerCase()] = value;
|
|
57
|
-
}
|
|
52
|
+
const cookieList = cookieValuesByName(cookiePairs);
|
|
53
|
+
const cookie = Object.create(null);
|
|
54
|
+
for (const [name, values] of Object.entries(cookieList))
|
|
55
|
+
cookie[name] = values[0];
|
|
56
|
+
const lowercasedHeaders = lowercaseHeaderNames(headers);
|
|
58
57
|
const header = (name) => lowercasedHeaders[name.toLowerCase()];
|
|
59
|
-
const
|
|
60
|
-
const ip = lowercasedHeaders["cf-connecting-ip"]
|
|
61
|
-
|| (forwardedFor ? (forwardedFor.split(",")[0] ?? "").trim() : "")
|
|
62
|
-
|| sourceIp
|
|
63
|
-
|| "";
|
|
58
|
+
const ip = resolveClientIp(lowercasedHeaders, sourceIp, trustedClientIpHeaders);
|
|
64
59
|
// Decode body: keep the raw string, then parse as JSON with urlencoded fallback.
|
|
65
|
-
|
|
60
|
+
const rawBody = event.isBase64Encoded
|
|
61
|
+
? (event.body ? base64ToText(event.body) : "")
|
|
62
|
+
: (event.body || "");
|
|
66
63
|
let post = {};
|
|
67
64
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const params = new URLSearchParams(rawBody);
|
|
76
|
-
post = {};
|
|
77
|
-
for (const [key, value] of params.entries()) {
|
|
78
|
-
post[key] = value;
|
|
79
|
-
}
|
|
65
|
+
post = JSON.parse(rawBody || "{}") || {};
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
const params = new URLSearchParams(rawBody);
|
|
69
|
+
post = {};
|
|
70
|
+
for (const [key, value] of params.entries()) {
|
|
71
|
+
post[key] = value;
|
|
80
72
|
}
|
|
81
73
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
// A POST to the API path whose body names an API is an API call; the
|
|
75
|
+
// core reads the envelope, and everything downstream reads ctx.api.
|
|
76
|
+
const api = method === "POST" && !!apiPath && path === apiPath
|
|
77
|
+
? readApiEnvelope(post, { headers: lowercasedHeaders, cookies: cookieList, ip, host })
|
|
78
|
+
: null;
|
|
87
79
|
return {
|
|
88
80
|
host, path, pathParams: {}, method,
|
|
89
81
|
get, post, cookie, cookieList, event,
|
|
90
82
|
session: null,
|
|
91
|
-
|
|
83
|
+
api,
|
|
84
|
+
apiName: api?.apiName ?? null,
|
|
85
|
+
apiPayload: api ? api.payload : null,
|
|
92
86
|
guardData: {},
|
|
93
87
|
headers, rawBody, ip, header,
|
|
94
88
|
lambdaContext,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
addHeaderFnAccumulator: [],
|
|
99
|
-
logToApiResponseAccumulator: [],
|
|
100
|
-
}
|
|
89
|
+
eventFormat,
|
|
90
|
+
responseHeaders: new LambderAnswerHeaders(),
|
|
91
|
+
logList: [],
|
|
101
92
|
};
|
|
102
93
|
};
|
|
103
|
-
/**
|
|
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.
|
|
110
|
-
*
|
|
111
|
-
* Every failure answers with a message instead of throwing: a malformed body
|
|
112
|
-
* is a client error, not a crash. The declared byte length both bounds the
|
|
113
|
-
* decompression and verifies it, so an over-large or tampered body is
|
|
114
|
-
* refused rather than expanded.
|
|
115
|
-
*/
|
|
116
|
-
export const restoreCompressedApiPayload = async (ctx, maxPayloadBytes) => {
|
|
117
|
-
const post = ctx.post;
|
|
118
|
-
const hasGzip = post[COMPRESSED_PAYLOAD_GZ_FIELD] !== undefined;
|
|
119
|
-
const hasBrotli = post[COMPRESSED_PAYLOAD_BR_FIELD] !== undefined;
|
|
120
|
-
if (!hasGzip && !hasBrotli)
|
|
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];
|
|
128
|
-
if (typeof compressed !== "string") {
|
|
129
|
-
return { ok: false, message: `Request ${field} must be a base64 string.` };
|
|
130
|
-
}
|
|
131
|
-
const declaredBytes = post[COMPRESSED_PAYLOAD_BYTES_FIELD];
|
|
132
|
-
if (typeof declaredBytes !== "number" || !Number.isSafeInteger(declaredBytes) || declaredBytes <= 0) {
|
|
133
|
-
return { ok: false, message: `Request ${COMPRESSED_PAYLOAD_BYTES_FIELD} must be the payload's byte length.` };
|
|
134
|
-
}
|
|
135
|
-
if (declaredBytes > maxPayloadBytes) {
|
|
136
|
-
return { ok: false, message: `Request payload of ${declaredBytes} bytes exceeds the ${maxPayloadBytes} byte limit.` };
|
|
137
|
-
}
|
|
138
|
-
// The bound and the exact-length verification are the codec's, the same
|
|
139
|
-
// ones a stored record gets; only the wording of the refusal is ours.
|
|
140
|
-
let json;
|
|
141
|
-
try {
|
|
142
|
-
json = await restoreText(Buffer.from(compressed, "base64"), encoding, { declaredBytes });
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const reason = err instanceof LambderCompressionError ? err.reason : null;
|
|
146
|
-
return { ok: false, message: reason === LAMBDER_RESTORE_FAILURES.lengthMismatch
|
|
147
|
-
? "Compressed request payload does not match its declared length."
|
|
148
|
-
: "Compressed request payload could not be decompressed." };
|
|
149
|
-
}
|
|
150
|
-
let payload;
|
|
151
|
-
try {
|
|
152
|
-
payload = JSON.parse(json);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return { ok: false, message: "Compressed request payload is not valid JSON." };
|
|
156
|
-
}
|
|
157
|
-
delete post[field];
|
|
158
|
-
delete post[COMPRESSED_PAYLOAD_BYTES_FIELD];
|
|
159
|
-
post.payload = payload;
|
|
160
|
-
ctx.apiPayload = payload;
|
|
161
|
-
return { ok: true };
|
|
162
|
-
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Context } from "aws-lambda";
|
|
3
|
+
import type LambderResolver from "./LambderResolver.js";
|
|
4
|
+
import type LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
5
|
+
import type { LambderResponse, LambderHttpResponse, LambderResponseCompressionOption, LambderResponseCompressionSettings } from "./LambderResponse.js";
|
|
6
|
+
import type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent } from "./LambderContext.js";
|
|
7
|
+
import type { LambderFilesOption } from "./LambderFiles.js";
|
|
8
|
+
import type { LambderCorsConfig } from "./LambderCors.js";
|
|
9
|
+
import type { LambderSessionDataRefreshConfig } from "../session/LambderSessionManager.js";
|
|
10
|
+
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
11
|
+
import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
|
|
12
|
+
import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
|
|
13
|
+
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
14
|
+
import type { LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig } from "../api/LambderApiRateLimits.js";
|
|
15
|
+
import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
|
|
16
|
+
import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
17
|
+
export type LambderRouteHandler = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
18
|
+
export type LambderSessionRouteHandler<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
19
|
+
export type LambderHookEvent = "created" | "beforeRender" | "afterRender" | "fallback";
|
|
20
|
+
/** Return the (possibly replaced) ctx to continue, a LambderResponse to short-circuit, or an Error to fail. */
|
|
21
|
+
export type LambderBeforeRenderHook = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderRenderContext | LambderResponse | Error>;
|
|
22
|
+
export type LambderAfterRenderHook = (ctx: LambderRenderContext, resolver: LambderResolver, response: LambderResponse) => MaybePromise<LambderResponse | Error>;
|
|
23
|
+
export type LambderFallbackHook = (ctx: LambderRenderContext, resolver: LambderResolver) => void | Promise<void>;
|
|
24
|
+
export type LambderGlobalErrorHandler = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder) => MaybePromise<LambderResponse>;
|
|
25
|
+
export type LambderFallbackHandler = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
26
|
+
export type LambderInputValidationHandler = (ctx: LambderRenderContext, resolver: LambderResolver, zodError: z.ZodError) => MaybePromise<LambderResponse>;
|
|
27
|
+
/**
|
|
28
|
+
* Second argument of an addAction handler. Discriminated on `ctx`: HTTP
|
|
29
|
+
* invocations get the full context and a resolver, non-HTTP invocations get
|
|
30
|
+
* null for both.
|
|
31
|
+
*/
|
|
32
|
+
export type LambderActionTools = {
|
|
33
|
+
ctx: LambderRenderContext;
|
|
34
|
+
res: LambderResolver;
|
|
35
|
+
lambdaContext: Context;
|
|
36
|
+
} | {
|
|
37
|
+
ctx: null;
|
|
38
|
+
res: null;
|
|
39
|
+
lambdaContext: Context;
|
|
40
|
+
};
|
|
41
|
+
export type LambderActionFilter = (event: unknown, ctx: LambderRenderContext | null) => boolean;
|
|
42
|
+
export type LambderActionHandler<TEvent = unknown> = (event: TEvent, tools: LambderActionTools) => MaybePromise<unknown>;
|
|
43
|
+
/** Overloaded handler type returned by getHandler(): HTTP events get a typed response, others dispatch to actions. */
|
|
44
|
+
export type LambderHandler = {
|
|
45
|
+
(event: LambderHttpEvent, context: Context): Promise<LambderHttpResponse>;
|
|
46
|
+
(event: unknown, context: Context): Promise<unknown>;
|
|
47
|
+
};
|
|
48
|
+
/** Session configuration (the `session` option of create/new): where sessions rest, and how their cookies are scoped. */
|
|
49
|
+
export type LambderSessionOptions<TSessionData = any> = {
|
|
50
|
+
/**
|
|
51
|
+
* Where sessions rest: a LambderDdbSessionStore over your table, a
|
|
52
|
+
* LambderMemorySessionStore in tests, or your own LambderSessionStore.
|
|
53
|
+
*
|
|
54
|
+
* Typed over `any` rather than over TSessionData deliberately. The session
|
|
55
|
+
* data type is the app's declaration (initLambder<SessionData>()), and a
|
|
56
|
+
* store is a storage backend that holds whatever the app puts in it;
|
|
57
|
+
* naming TSessionData here would make `new Lambder({ session: { store } })`
|
|
58
|
+
* INFER the session data type from the store instead, so an app that never
|
|
59
|
+
* said otherwise would find ctx.session.data typed by its table.
|
|
60
|
+
*/
|
|
61
|
+
store: LambderSessionStore<any>;
|
|
62
|
+
/** Salts the sessionKey hash that partitions the store. */
|
|
63
|
+
sessionSalt: string;
|
|
64
|
+
enableSlidingExpiration?: boolean;
|
|
65
|
+
/** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
|
|
66
|
+
slidingWriteIntervalSeconds?: number;
|
|
67
|
+
/** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. `domain` may be a (hostname) => string function for multi-domain deployments. */
|
|
68
|
+
cookie?: LambderSessionCookieOptions;
|
|
69
|
+
/** Session cookie names. Defaults: LMDRSESSIONTKID / LMDRSESSIONCSTK. */
|
|
70
|
+
tokenCookieKey?: string;
|
|
71
|
+
csrfCookieKey?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Opt-in freshness for session.data derived from external state (roles,
|
|
74
|
+
* permissions, feature flags...). Every session read renews data past
|
|
75
|
+
* its ttlSeconds via your refresh callback, persisting in place on the
|
|
76
|
+
* same record: same tokens, same cookies. Return null from refresh to
|
|
77
|
+
* end the session. See LambderSessionDataRefreshConfig for the exact
|
|
78
|
+
* semantics.
|
|
79
|
+
*/
|
|
80
|
+
dataRefresh?: LambderSessionDataRefreshConfig<TSessionData>;
|
|
81
|
+
/** Hashing and randomness for the session tokens. Default: WebCrypto. */
|
|
82
|
+
crypto?: LambderSessionCrypto;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Everything an instance is configured with, in ONE declaration: base
|
|
86
|
+
* serving options plus the type-affecting policy layer (rate limits,
|
|
87
|
+
* guards, idempotency) and session/CORS config. There are no enable/define
|
|
88
|
+
* chain methods; the instance is born fully configured and fully typed
|
|
89
|
+
* (via initLambder), so no ordering rules exist and no partially-configured
|
|
90
|
+
* instance type ever needs a name.
|
|
91
|
+
*/
|
|
92
|
+
export type LambderCreateOptions<TSessionData = any> = {
|
|
93
|
+
/**
|
|
94
|
+
* Where the app's files come from, for servePublicFiles, serveIndexHtml,
|
|
95
|
+
* res.file and res.templateFile: a LambderLocalFileSource over a folder
|
|
96
|
+
* (the build output bundled with the deployment), a LambderS3FileSource
|
|
97
|
+
* (S3, R2), or any LambderFileSource; or `{ source, memoryCache }` to
|
|
98
|
+
* tune or disable the in-memory file cache. Required by those features.
|
|
99
|
+
*/
|
|
100
|
+
files?: LambderFilesOption;
|
|
101
|
+
apiPath?: string;
|
|
102
|
+
apiVersion?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Automatic compression for compressible responses. `true` (the default)
|
|
105
|
+
* is `{ minBytes: 860, encodings: ["br", "gzip"], quality: 5 }`; `false`
|
|
106
|
+
* disables it. `encodings` is a preference order, so `["gzip"]` opts out
|
|
107
|
+
* of Brotli for a client or CDN that mishandles it, and `quality` is the
|
|
108
|
+
* Brotli quality, the same field the at-rest stores take.
|
|
109
|
+
*/
|
|
110
|
+
compression?: LambderResponseCompressionOption;
|
|
111
|
+
/** Automatic ETag + If-None-Match 304 on GET/HEAD 200 responses. Default: true. */
|
|
112
|
+
etag?: boolean;
|
|
113
|
+
/** Guard threshold for Lambda's ~6MB response cap. Default: 5,500,000. */
|
|
114
|
+
maxResponseBytes?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Ceiling on what a gzipped request payload may restore to (Lambda's
|
|
117
|
+
* ~6MB invoke cap already bounds the compressed bytes). Default:
|
|
118
|
+
* 20,000,000. Requests over it are refused rather than decompressed.
|
|
119
|
+
* The restored JSON is parsed in full before any policy or session
|
|
120
|
+
* check, so size it to the function's memory.
|
|
121
|
+
*/
|
|
122
|
+
maxRequestPayloadBytes?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Headers that may name the caller's own address, in order of preference,
|
|
125
|
+
* e.g. ["cf-connecting-ip"] behind Cloudflare or ["x-forwarded-for"]
|
|
126
|
+
* behind a proxy that rewrites it. Default: none, so ctx.ip is the address
|
|
127
|
+
* the gateway observed.
|
|
128
|
+
*
|
|
129
|
+
* Only list a header something in front of this app always overwrites. A
|
|
130
|
+
* header a client can set is a value a client can choose, and `per: "ip"`
|
|
131
|
+
* rate limits key off ctx.ip: one that a caller picks per request is not a
|
|
132
|
+
* limit. Note that API Gateway APPENDS to x-forwarded-for rather than
|
|
133
|
+
* replacing it, so behind API Gateway alone the leftmost entry is the
|
|
134
|
+
* client's own claim and the header should be left out.
|
|
135
|
+
*/
|
|
136
|
+
trustedClientIpHeaders?: readonly string[];
|
|
137
|
+
/** CORS: true allows any origin; or pass a LambderCorsConfig. Default: off. */
|
|
138
|
+
cors?: boolean | LambderCorsConfig;
|
|
139
|
+
/** Sessions over a store of your choosing; required for addSessionApi/addSessionRoute. */
|
|
140
|
+
session?: LambderSessionOptions<TSessionData>;
|
|
141
|
+
/** Declarative per-API rate limiting: your limiter plus named policies APIs reference (typed) via the `rateLimit` option. */
|
|
142
|
+
rateLimits?: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig<LambderRenderContext>>>;
|
|
143
|
+
/** Named guards APIs reference (typed) via the `guards` option; build each with lambderGuard(). Pinned to the render contexts, so a guard built for another adapter is rejected here rather than reading fields that are not on its context. */
|
|
144
|
+
guards?: Record<string, LambderApiGuard<any, any, any, LambderRenderContext, LambderSessionRenderContext<any, any>>>;
|
|
145
|
+
/**
|
|
146
|
+
* Make an authorization declaration part of registering a session API:
|
|
147
|
+
* every addSessionApi must declare `guards`, at the type level (a
|
|
148
|
+
* missing `guards` is a compile error) and at registration (a plain-JS
|
|
149
|
+
* caller throws). An API that legitimately needs none, because the
|
|
150
|
+
* session itself is the whole authorization (the signed-in user's own
|
|
151
|
+
* account), declares a named no-op session guard, so every opt-out is
|
|
152
|
+
* explicit and one grep lists them all. Needs a guards map to pick
|
|
153
|
+
* from. Default: false.
|
|
154
|
+
*/
|
|
155
|
+
requireSessionApiGuards?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* The same for public APIs: every addApi must declare `guards`, at the
|
|
158
|
+
* type level and at registration.
|
|
159
|
+
*
|
|
160
|
+
* Public APIs are open by default and that is the right default, so this
|
|
161
|
+
* is off unless an app decides otherwise. What it buys an app that turns
|
|
162
|
+
* it on is that a public endpoint's openness becomes a written decision
|
|
163
|
+
* rather than an omission: the ones anybody may call declare a named no-op
|
|
164
|
+
* guard carrying the reason, and the ones that authorize their caller some
|
|
165
|
+
* other way (a signature, a device secret, a one-shot token) name where
|
|
166
|
+
* that happens. One grep over the guard names then lists every public
|
|
167
|
+
* door and why it is open, which is the review question a growing public
|
|
168
|
+
* surface makes expensive to answer any other way. Needs a guards map to
|
|
169
|
+
* pick from. Default: false.
|
|
170
|
+
*/
|
|
171
|
+
requirePublicApiGuards?: boolean;
|
|
172
|
+
/** Declarative idempotency: your store plus replay defaults; APIs opt in via `idempotency: true | { ttlSeconds }`. */
|
|
173
|
+
idempotency?: LambderApiIdempotencyConfig;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* What the `guards` field asks for when an API on a require*ApiGuards
|
|
177
|
+
* instance declares none. The inference parameter defaults to `never` with
|
|
178
|
+
* nothing to infer from, and the resulting "Property 'guards' is missing ...
|
|
179
|
+
* but required in type { guards: never }" read as though nothing could ever
|
|
180
|
+
* be written there; the property name says what is actually wanted.
|
|
181
|
+
*/
|
|
182
|
+
type LambderGuardsDeclarationRequired = {
|
|
183
|
+
readonly "lambder: this instance requires every API of this kind to declare guards. Name the guard that authorizes this API, or the named no-op guard that records why anyone may call it.": never;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* What addSessionApi and addSessionRoute need of the instance they are called
|
|
187
|
+
* on. Nothing satisfies it without the session option, so registering a
|
|
188
|
+
* session API on an instance that has no sessions is a compile error rather
|
|
189
|
+
* than only the registration-time throw.
|
|
190
|
+
*/
|
|
191
|
+
type LambderSessionOptionRequired = {
|
|
192
|
+
readonly "lambder: sessions are not configured on this instance. Pass the session option to create() before registering a session API or a session route.": never;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Intersected into what addSessionApi and addSessionRoute take, so an
|
|
196
|
+
* instance created without the session option refuses the registration at
|
|
197
|
+
* the call site. `unknown` once sessions are configured, which intersects
|
|
198
|
+
* away to nothing.
|
|
199
|
+
*/
|
|
200
|
+
export type LambderSessionEnabledInstance<TSessionsEnabled extends boolean> = TSessionsEnabled extends true ? unknown : LambderSessionOptionRequired;
|
|
201
|
+
/**
|
|
202
|
+
* The `guards` field of an API's options: optional by default, required once
|
|
203
|
+
* create() received the require*ApiGuards flag for that kind of API, so that
|
|
204
|
+
* an authorization declaration cannot be forgotten at the type level.
|
|
205
|
+
*
|
|
206
|
+
* One type for both kinds: the requirement is the same shape either way, and
|
|
207
|
+
* only which flag switches it on differs.
|
|
208
|
+
*/
|
|
209
|
+
export type LambderRequirableGuardsField<TRequired extends boolean, TGuardsOpt> = TRequired extends true ? {
|
|
210
|
+
/** Named guards, run in declared order before input validation: a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Required on this instance: an API that needs no authorization declares a named no-op guard, so every opt-out is explicit and one grep lists them all. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
211
|
+
guards: [TGuardsOpt] extends [never] ? LambderGuardsDeclarationRequired : TGuardsOpt;
|
|
212
|
+
} : {
|
|
213
|
+
/** Named guards, run in declared order before input validation: a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
214
|
+
guards?: TGuardsOpt;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Rejects a key the options type does not have, which `const TOptions` would
|
|
218
|
+
* otherwise wave through: inferring a generic from an object literal switches
|
|
219
|
+
* excess-property checking off for the whole literal, so `requireSessionApiGuard`
|
|
220
|
+
* (no trailing "s") or `maxResponseByte` would compile, be dropped in silence,
|
|
221
|
+
* and leave the app running with the default. That matters most for exactly
|
|
222
|
+
* the two flags a typo is worst on, since both exist to make a missing
|
|
223
|
+
* authorization declaration a compile error. Mapping every surplus key to
|
|
224
|
+
* `never` puts the error back on the key itself.
|
|
225
|
+
*/
|
|
226
|
+
export type LambderNoExtraKeys<TOptions, TShape> = TOptions & Record<Exclude<keyof TOptions, keyof TShape>, never>;
|
|
227
|
+
/**
|
|
228
|
+
* What create() was actually given for one option, or undefined when the
|
|
229
|
+
* literal does not carry the key at all.
|
|
230
|
+
*
|
|
231
|
+
* `TOptions[TKey]` on its own answers with the CONSTRAINT's type for a key
|
|
232
|
+
* the literal omits, so a flag nobody passed reads as `boolean | undefined`,
|
|
233
|
+
* which is "somebody might have set it" rather than "nobody did".
|
|
234
|
+
*/
|
|
235
|
+
export type LambderGivenOption<TOptions, TKey extends PropertyKey> = TKey extends keyof TOptions ? TOptions[TKey] : undefined;
|
|
236
|
+
/**
|
|
237
|
+
* One option's declared shape, read back off LambderCreateOptions rather than
|
|
238
|
+
* named a second time, so the nested checks below cannot drift from the
|
|
239
|
+
* option they check.
|
|
240
|
+
*/
|
|
241
|
+
type LambderOptionShape<TSessionData, TKey extends keyof LambderCreateOptions<TSessionData>> = NonNullable<LambderCreateOptions<TSessionData>[TKey]>;
|
|
242
|
+
/**
|
|
243
|
+
* The surplus-key rule one level down, over the option objects a typo is
|
|
244
|
+
* worst on.
|
|
245
|
+
*
|
|
246
|
+
* Excess-property checking is off for the WHOLE literal under `const
|
|
247
|
+
* TOptions`, nested objects included, so the top-level rule caught nothing
|
|
248
|
+
* where it mattered most: `idempotency: { failOpn: false }` left the engine
|
|
249
|
+
* failing open, `callerIdentitiy` left every public replay key a bearer
|
|
250
|
+
* token, `session: { tokenCookieKe }` left the session cookie under its
|
|
251
|
+
* default name, and `guards: { g: { sesion: true } }` left a guard reading a
|
|
252
|
+
* context with no session. Each nested object is checked against the shape
|
|
253
|
+
* its own option declares, so the error lands on the misspelled key.
|
|
254
|
+
*/
|
|
255
|
+
export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCreateOptions<TSessionData>> = {
|
|
256
|
+
session?: LambderNoExtraKeys<NonNullable<TOptions["session"]>, LambderOptionShape<TSessionData, "session">> & {
|
|
257
|
+
cookie?: LambderNoExtraKeys<NonNullable<NonNullable<TOptions["session"]>["cookie"]>, LambderSessionCookieOptions>;
|
|
258
|
+
};
|
|
259
|
+
idempotency?: LambderNoExtraKeys<NonNullable<TOptions["idempotency"]>, LambderOptionShape<TSessionData, "idempotency">>;
|
|
260
|
+
rateLimits?: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>, LambderOptionShape<TSessionData, "rateLimits">> & {
|
|
261
|
+
policies?: {
|
|
262
|
+
[TPolicy in keyof NonNullable<TOptions["rateLimits"]>["policies"]]: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>["policies"][TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext>>;
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
guards?: {
|
|
266
|
+
[TGuard in keyof NonNullable<TOptions["guards"]>]: LambderNoExtraKeys<NonNullable<TOptions["guards"]>[TGuard], LambderOptionShape<TSessionData, "guards">[string]>;
|
|
267
|
+
};
|
|
268
|
+
files?: TOptions["files"] extends {
|
|
269
|
+
source: unknown;
|
|
270
|
+
} ? LambderNoExtraKeys<TOptions["files"], Extract<LambderFilesOption, {
|
|
271
|
+
source: unknown;
|
|
272
|
+
}>> : unknown;
|
|
273
|
+
cors?: TOptions["cors"] extends object ? LambderNoExtraKeys<TOptions["cors"], LambderCorsConfig> : unknown;
|
|
274
|
+
compression?: TOptions["compression"] extends object ? LambderNoExtraKeys<TOptions["compression"], LambderResponseCompressionSettings> : unknown;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Everything create() refuses before an instance exists.
|
|
278
|
+
*
|
|
279
|
+
* One place rather than five checks spread through the constructor's wiring:
|
|
280
|
+
* a value that cannot work is a startup error naming the option, not a 404 on
|
|
281
|
+
* every API call (an apiPath with no leading slash) or a 500 on every response
|
|
282
|
+
* (maxResponseBytes: 0) that an app discovers in production.
|
|
283
|
+
*/
|
|
284
|
+
export declare const assertCreateOptions: (options: LambderCreateOptions<any>) => void;
|
|
285
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
2
|
+
/**
|
|
3
|
+
* The session fields that moved onto LambderDdbSessionStore in 7.0.0, refused
|
|
4
|
+
* at creation. They are the one break the compiler cannot find: create() is
|
|
5
|
+
* generic over `const TOptions`, which switches excess-property checking off
|
|
6
|
+
* for the whole options object, so a leftover `partitionKey` compiles and
|
|
7
|
+
* would be dropped in silence while the store fell back to its own table
|
|
8
|
+
* defaults. A wrong table key is not something to discover from nobody being
|
|
9
|
+
* able to log in.
|
|
10
|
+
*/
|
|
11
|
+
const MOVED_SESSION_OPTIONS = ["tableName", "tableRegion", "partitionKey", "sortKey", "compression"];
|
|
12
|
+
/**
|
|
13
|
+
* Everything create() refuses before an instance exists.
|
|
14
|
+
*
|
|
15
|
+
* One place rather than five checks spread through the constructor's wiring:
|
|
16
|
+
* a value that cannot work is a startup error naming the option, not a 404 on
|
|
17
|
+
* every API call (an apiPath with no leading slash) or a 500 on every response
|
|
18
|
+
* (maxResponseBytes: 0) that an app discovers in production.
|
|
19
|
+
*/
|
|
20
|
+
export const assertCreateOptions = (options) => {
|
|
21
|
+
// The path is compared to ctx.path, which always starts with a slash, so
|
|
22
|
+
// apiPath: "api" made every API call a 404 and nothing said why.
|
|
23
|
+
if (options.apiPath !== undefined && (options.apiPath === "" || !options.apiPath.startsWith("/"))) {
|
|
24
|
+
throw new Error(`Lambder: apiPath must be a path starting with "/", got ${JSON.stringify(options.apiPath)}.`);
|
|
25
|
+
}
|
|
26
|
+
// "" is the one string that turns the version gate off while looking like
|
|
27
|
+
// it was set; say so rather than accepting every version a client names.
|
|
28
|
+
if (options.apiVersion === "") {
|
|
29
|
+
throw new Error("Lambder: apiVersion must not be empty. Leave it out to run without the version gate.");
|
|
30
|
+
}
|
|
31
|
+
// 0 or a negative ceiling turned every response into the size guard's own 500.
|
|
32
|
+
if (options.maxResponseBytes !== undefined)
|
|
33
|
+
assertPositiveInteger(options.maxResponseBytes, "maxResponseBytes");
|
|
34
|
+
const session = options.session;
|
|
35
|
+
const movedOptions = session ? MOVED_SESSION_OPTIONS.filter((key) => key in session) : [];
|
|
36
|
+
if (movedOptions.length) {
|
|
37
|
+
throw new Error(`Lambder: the session option no longer takes ${movedOptions.join(", ")}. `
|
|
38
|
+
+ "They belong to the store now: session: { store: new LambderDdbSessionStore({ tableName, region, partitionKey, sortKey, compression }), sessionSalt }.");
|
|
39
|
+
}
|
|
40
|
+
if ((options.requireSessionApiGuards || options.requirePublicApiGuards) && !options.guards) {
|
|
41
|
+
const requireFlag = options.requireSessionApiGuards ? "requireSessionApiGuards" : "requirePublicApiGuards";
|
|
42
|
+
throw new Error(`Lambder: ${requireFlag} needs a guards map at creation for APIs to declare from.`);
|
|
43
|
+
}
|
|
44
|
+
};
|