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.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +21 -19
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /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 { LambderSessionContext } from "../session/LambderSessionManager.js";
3
- import type { LambderHttpEventFormat } from "./LambderResponse.js";
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
- export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}> = {
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, any>;
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
- session: null;
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
- /** Client IP: CF-Connecting-IP, then X-Forwarded-For, then the API Gateway source IP. */
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
- _otherInternal: {
43
- isApiCall: boolean;
44
- requestVersion: string | null;
45
- eventFormat: LambderHttpEventFormat;
46
- setHeaderFnAccumulator: {
47
- key: string;
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: LambderSessionContext<SessionData>;
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
- export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
61
- /** Outcome of restoring a compressed request payload; the message is client-facing. */
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 cookieParser from "cookie";
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";
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
- export const createContext = (event, lambdaContext, apiPath) => {
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
- cookiePairs = (headers.Cookie || headers.cookie || "").split(";");
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
- // Parsed pair by pair so a name that arrived more than once keeps every
44
- // value; a whole-header parse keeps only the first.
45
- const cookieList = {};
46
- for (const pair of cookiePairs) {
47
- for (const [name, value] of Object.entries(cookieParser.parse(pair))) {
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 forwardedFor = lowercasedHeaders["x-forwarded-for"];
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
- let rawBody = "";
60
+ const rawBody = event.isBase64Encoded
61
+ ? (event.body ? base64ToText(event.body) : "")
62
+ : (event.body || "");
66
63
  let post = {};
67
64
  try {
68
- rawBody = event.isBase64Encoded
69
- ? (event.body ? Buffer.from(event.body, "base64").toString() : "")
70
- : (event.body || "");
71
- try {
72
- post = JSON.parse(rawBody || "{}") || {};
73
- }
74
- catch (e) {
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
- catch (e) { }
83
- const isApiCall = !!(method === "POST" && apiPath && path === apiPath && post.apiName);
84
- const apiName = isApiCall ? post.apiName : null;
85
- const apiPayload = isApiCall ? post.payload : null;
86
- const requestVersion = isApiCall ? (post.version ?? null) : null;
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
- apiName, apiPayload,
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
- _otherInternal: {
96
- isApiCall, requestVersion, eventFormat,
97
- setHeaderFnAccumulator: [],
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
+ };