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,27 +1,5 @@
1
1
  import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
2
- /** A file a source serves: its bytes, and its mime type when the source knows it (otherwise resolved from the extension). */
3
- export type LambderFile = {
4
- body: Buffer;
5
- mimeType?: string;
6
- };
7
- /**
8
- * Where an app's files come from: the `files` option at creation, read by
9
- * servePublicFiles, serveIndexHtml, res.file and res.templateFile alike,
10
- * through the instance's one reader (LambderFiles). Implement `read` over
11
- * any backing store: LambderLocalFileSource (a folder), LambderS3FileSource
12
- * (S3, or R2 and other S3-compatible stores), LambderHttpFileSource (any
13
- * origin serving files by path), or your own. The reader does the rest for
14
- * every source: traversal check, memory cache, mime fallback from the
15
- * extension.
16
- */
17
- export interface LambderFileSource {
18
- /**
19
- * The file at a relative path (no leading slash, no ".." segments: the
20
- * reader rejects those before calling), or null when there is no such
21
- * file, which lets a request fall through to the route fallback.
22
- */
23
- read(relativePath: string): Promise<LambderFile | null>;
24
- }
2
+ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
25
3
  /** In-memory cache of files for warm invocations. Default: { maxBytes: 32MB, maxFileBytes: 2MB }. false disables it. */
26
4
  export type LambderFileMemoryCacheOption = false | {
27
5
  maxBytes?: number;
@@ -38,28 +16,6 @@ export type LambderReadFile = {
38
16
  mimeType: string;
39
17
  relativePath: string;
40
18
  };
41
- /**
42
- * Files from a folder on the Lambda's filesystem, typically the build output
43
- * bundled into the deployment package. Reads stay under root.
44
- */
45
- export declare class LambderLocalFileSource implements LambderFileSource {
46
- private root;
47
- constructor({ root }: {
48
- root: string;
49
- });
50
- read(relativePath: string): Promise<LambderFile | null>;
51
- }
52
- /**
53
- * A file a remote store returned: the store's Content-Type unless it is a
54
- * generic octet-stream, in which case the extension decides, as for local
55
- * files.
56
- */
57
- export declare const remoteStoreFile: (body: Buffer, contentType: string | null | undefined) => LambderFile;
58
- /**
59
- * The path a source is asked for: leading slash stripped, traversal
60
- * rejected; null for a path that names no file (empty, or a directory).
61
- */
62
- export declare const toRelativePath: (target: string) => string | null;
63
19
  /**
64
20
  * The app's file reader, owned by the Lambder instance: one source, one
65
21
  * path rule, one memory cache and one compiled-template cache, shared by
@@ -1,48 +1,28 @@
1
1
  import mimeTypeResolver from "mime-types";
2
- import { getFS, getPath } from "../shared/node-polyfills.js";
3
2
  import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
4
3
  const DEFAULT_MEMORY_CACHE_MAX_BYTES = 32 * 1024 * 1024;
5
4
  const DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES = 2 * 1024 * 1024;
6
5
  /**
7
- * Files from a folder on the Lambda's filesystem, typically the build output
8
- * bundled into the deployment package. Reads stay under root.
6
+ * The path a source is asked for, or null for one that names no file.
7
+ *
8
+ * This is the whole path rule, and it belongs to the reader: every source is
9
+ * handed the result, and a source that resolves it against a base (a URL, a
10
+ * filesystem root) is safe only if the value really is the plain relative
11
+ * path the interface promises. Stripping only ONE leading slash would hand a
12
+ * source "//attacker.example/evil.html" as "/attacker.example/evil.html",
13
+ * which the HTTP source resolves as a protocol-relative reference: the app's
14
+ * origin credentials would go to a host the caller chose and its bytes would
15
+ * come back under the app's own domain. So every leading slash goes, and every segment is
16
+ * checked rather than only the ".." ones: an empty inner segment is how a
17
+ * host or a root gets back into the value, and a backslash is a separator to
18
+ * Windows paths and to every browser reading a Location.
9
19
  */
10
- export class LambderLocalFileSource {
11
- root;
12
- constructor({ root }) {
13
- this.root = root;
14
- }
15
- async read(relativePath) {
16
- const fs = await getFS();
17
- const path = await getPath();
18
- if (!fs || !path)
19
- throw new Error("LambderLocalFileSource requires a Node.js environment.");
20
- const base = path.resolve(this.root);
21
- const absolute = path.resolve(base, relativePath);
22
- if (absolute !== base && !absolute.startsWith(base + path.sep))
23
- return null;
24
- const stat = await fs.promises.stat(absolute).catch(() => null);
25
- if (!stat?.isFile())
26
- return null;
27
- return { body: await fs.promises.readFile(absolute) };
28
- }
29
- }
30
- /**
31
- * A file a remote store returned: the store's Content-Type unless it is a
32
- * generic octet-stream, in which case the extension decides, as for local
33
- * files.
34
- */
35
- export const remoteStoreFile = (body, contentType) => contentType && !contentType.endsWith("octet-stream") ? { body, mimeType: contentType } : { body };
36
- /**
37
- * The path a source is asked for: leading slash stripped, traversal
38
- * rejected; null for a path that names no file (empty, or a directory).
39
- */
40
- export const toRelativePath = (target) => {
41
- if (target.split("/").some((segment) => segment === ".."))
42
- return null;
43
- const relative = target.startsWith("/") ? target.slice(1) : target;
20
+ const toRelativePath = (target) => {
21
+ const relative = target.replace(/^\/+/, "");
44
22
  if (relative === "" || relative.endsWith("/"))
45
23
  return null;
24
+ if (relative.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || segment.includes("\\")))
25
+ return null;
46
26
  return relative;
47
27
  };
48
28
  /**
@@ -99,7 +79,7 @@ export class LambderFiles {
99
79
  return cached;
100
80
  const file = await this.read(path);
101
81
  if (!file)
102
- throw new Error(`templateFile: file not found in the files source: ${path}`);
82
+ throw new Error(`Lambder: res.templateFile found no such file in the files source: ${path}`);
103
83
  const template = new LambderTemplatingEngine(file.body.toString("utf8"), { htmlVirtualSlots: options.htmlVirtualSlots });
104
84
  this.templates.set(key, template);
105
85
  return template;
@@ -0,0 +1,37 @@
1
+ import { type LambderRenderContext } from "./LambderContext.js";
2
+ import type LambderResolver from "./LambderResolver.js";
3
+ import type { LambderResponse } from "./LambderResponse.js";
4
+ import type { LambderFallbackHandler } from "./LambderCreateOptions.js";
5
+ export type LambderIndexHtmlOptions = {
6
+ /** Methods that reach the index handler. Default: ["GET", "HEAD"]; a list that names GET and not HEAD accepts HEAD too, as a route matcher's `method` does. */
7
+ methods?: string[];
8
+ /**
9
+ * Skip paths whose last segment contains a dot, treating them as missing
10
+ * assets rather than app routes. Default: false, because real files have already
11
+ * been served by servePublicFiles at this point, and plenty of app routes
12
+ * carry dots (JWTs, coordinates, domain names, version numbers). Turn it
13
+ * on to get 404s instead of a 200 shell for missing-asset requests.
14
+ */
15
+ skipFilePaths?: boolean;
16
+ /** 301-redirect trailing-slash paths to the canonical no-slash URL. Default: false. */
17
+ redirectTrailingSlash?: boolean;
18
+ /** Shell served by the default handler. Default: "index.html". */
19
+ indexFile?: string | ((ctx: LambderRenderContext) => string);
20
+ /** Compression override, like servePublicFiles: "auto" (default), true/false, or (ctx) => boolean | "auto". */
21
+ compress?: boolean | "auto" | ((ctx: LambderRenderContext) => boolean | "auto");
22
+ };
23
+ /**
24
+ * Serves the app shell for page requests that nothing else handled,
25
+ * registered via lambder.serveIndexHtml(). Runs after servePublicFiles in the
26
+ * fallback chain, so real files are already gone and everything left is an
27
+ * app route. A request the gates exclude (a method that is not configured, a
28
+ * dotted path under skipFilePaths) falls through to the route fallback.
29
+ */
30
+ export declare class LambderIndexHtmlHandler {
31
+ private handler;
32
+ private options;
33
+ private methods;
34
+ constructor(handler: LambderFallbackHandler | null, options: LambderIndexHtmlOptions);
35
+ /** Serve the shell, or return null to fall through. */
36
+ handle(ctx: LambderRenderContext, resolver: LambderResolver): Promise<LambderResponse | null>;
37
+ }
@@ -0,0 +1,87 @@
1
+ import { isV2HttpEvent } from "./LambderContext.js";
2
+ import { allowsRequestMethod } from "./LambderRouting.js";
3
+ /**
4
+ * Serves the app shell for page requests that nothing else handled,
5
+ * registered via lambder.serveIndexHtml(). Runs after servePublicFiles in the
6
+ * fallback chain, so real files are already gone and everything left is an
7
+ * app route. A request the gates exclude (a method that is not configured, a
8
+ * dotted path under skipFilePaths) falls through to the route fallback.
9
+ */
10
+ export class LambderIndexHtmlHandler {
11
+ handler;
12
+ options;
13
+ methods;
14
+ constructor(handler, options) {
15
+ this.handler = handler;
16
+ this.options = options;
17
+ this.methods = new Set((options.methods ?? ["GET", "HEAD"]).map((method) => method.toUpperCase()));
18
+ }
19
+ /** Serve the shell, or return null to fall through. */
20
+ async handle(ctx, resolver) {
21
+ const { handler, options } = this;
22
+ if (!allowsRequestMethod(this.methods, ctx.method))
23
+ return null;
24
+ if ((options.skipFilePaths ?? false) && (ctx.path.split("/").pop() ?? "").includes("."))
25
+ return null;
26
+ if (options.redirectTrailingSlash && ctx.path.length > 1 && ctx.path.endsWith("/")) {
27
+ const target = sameOriginRedirectTarget(ctx.path);
28
+ if (target !== null)
29
+ return resolver.redirect(target + buildQueryString(ctx), 301);
30
+ }
31
+ const response = handler
32
+ ? await handler(ctx, resolver)
33
+ : await resolver.templateFile(typeof options.indexFile === "function" ? options.indexFile(ctx) : (options.indexFile ?? "index.html"), {}, { cacheControl: "no-cache" });
34
+ if (options.compress !== undefined) {
35
+ response.compress = typeof options.compress === "function" ? options.compress(ctx) : options.compress;
36
+ }
37
+ return response;
38
+ }
39
+ }
40
+ /**
41
+ * The canonical no-trailing-slash form of a request path, as a Location that
42
+ * cannot leave this origin, or null when no such form exists.
43
+ *
44
+ * `Location: //evil.example` is a protocol-relative URL, so a browser
45
+ * navigates to that host; every browser normalizes backslashes into slashes
46
+ * first, so `/\evil.example` is the same thing. This header is built from the
47
+ * request path, which the caller writes, so redirectTrailingSlash was an open
48
+ * redirect for anyone who asked for `GET //evil.example/`. The leading run of
49
+ * slashes and backslashes collapses to the single slash a path may have, and
50
+ * the result is then checked rather than assumed: the check is what the
51
+ * header's safety rests on, and it costs one comparison.
52
+ */
53
+ const sameOriginRedirectTarget = (path) => {
54
+ const target = path.replace(/^[/\\]+/, "/").replace(/[/\\]+$/, "") || "/";
55
+ if (target.startsWith("//") || target.startsWith("/\\"))
56
+ return null;
57
+ return target;
58
+ };
59
+ /**
60
+ * Rebuild the query string from the API Gateway event for redirects.
61
+ *
62
+ * From the raw event rather than from ctx.get, which has flattened repeated
63
+ * keys to one value each and lost the order they arrived in; a redirect has
64
+ * to hand back the query it was given.
65
+ */
66
+ const buildQueryString = (ctx) => {
67
+ if (isV2HttpEvent(ctx.event)) {
68
+ return ctx.event.rawQueryString ? `?${ctx.event.rawQueryString}` : "";
69
+ }
70
+ const multi = ctx.event.multiValueQueryStringParameters;
71
+ const single = ctx.event.queryStringParameters;
72
+ const params = new URLSearchParams();
73
+ if (multi) {
74
+ for (const [key, values] of Object.entries(multi)) {
75
+ for (const value of values ?? [])
76
+ params.append(key, value);
77
+ }
78
+ }
79
+ else if (single) {
80
+ for (const [key, value] of Object.entries(single)) {
81
+ if (value !== undefined)
82
+ params.append(key, value);
83
+ }
84
+ }
85
+ const queryString = params.toString();
86
+ return queryString ? `?${queryString}` : "";
87
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The server's policy builders: the generic builders from `api/`, bound to
3
+ * the render contexts a Lambda handler runs on.
4
+ *
5
+ * They live in `core/` rather than beside the engines because binding them is
6
+ * the one thing about a guard or a rate-limit key that is the SERVER's, and
7
+ * having them in `api/` was the last reason that layer imported from `core/`
8
+ * at all. The mock runtime binds the same builders to its own call contexts,
9
+ * in its own layer, which is why the builders themselves are generic.
10
+ */
11
+ import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
12
+ import { type LambderGuardBuilder } from "../api/LambderApiGuards.js";
13
+ import { type LambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
14
+ /** Builder for the server's guards: the handler sees the render context (session-typed when `session: true`). */
15
+ export declare const lambderGuard: LambderGuardBuilder<LambderRenderContext, LambderSessionRenderContext<any, any>>;
16
+ /** Builder for the server's rate-limit keys: the handler sees the render context. */
17
+ export declare const lambderRateLimitKey: LambderRateLimitKeyBuilder<LambderRenderContext>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The server's policy builders: the generic builders from `api/`, bound to
3
+ * the render contexts a Lambda handler runs on.
4
+ *
5
+ * They live in `core/` rather than beside the engines because binding them is
6
+ * the one thing about a guard or a rate-limit key that is the SERVER's, and
7
+ * having them in `api/` was the last reason that layer imported from `core/`
8
+ * at all. The mock runtime binds the same builders to its own call contexts,
9
+ * in its own layer, which is why the builders themselves are generic.
10
+ */
11
+ import { lambderGuardBuilder } from "../api/LambderApiGuards.js";
12
+ import { lambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
13
+ /** Builder for the server's guards: the handler sees the render context (session-typed when `session: true`). */
14
+ export const lambderGuard = lambderGuardBuilder();
15
+ /** Builder for the server's rate-limit keys: the handler sees the render context. */
16
+ export const lambderRateLimitKey = lambderRateLimitKeyBuilder();
@@ -3,6 +3,8 @@ import type { LambderFiles } from "./LambderFiles.js";
3
3
  import { LambderResponse } from "./LambderResponse.js";
4
4
  /** Per-registration policy of servePublicFiles: how a request maps to a file and how the response is cached. */
5
5
  export type LambderPublicFilesOptions = {
6
+ /** Methods that reach the public-file layer. Default: ["GET", "HEAD"], the same gate and the same default its serveIndexHtml sibling has. */
7
+ methods?: string[];
6
8
  /**
7
9
  * Map the request to a file path (app-owned logic, e.g. per-tenant
8
10
  * roots: (ctx) => `${brand(ctx.host)}${ctx.path}`). Return
@@ -24,14 +26,15 @@ export type LambderPublicFilesOptions = {
24
26
  /**
25
27
  * Terminal public-file handler registered via lambder.servePublicFiles().
26
28
  * Runs only when no route matched, so it can never shadow routes registered
27
- * after it. Serves files through the instance's reader (traversal-safe,
29
+ * after it. Serves files through the instance's reader (one path rule,
28
30
  * mime-typed, memory-cached) with the immutable-cache heuristic for
29
31
  * content-hashed assets, and falls through to the route fallback when the
30
- * source has no such file.
32
+ * method is not configured or the source has no such file.
31
33
  */
32
34
  export declare class LambderPublicFilesHandler {
33
35
  private files;
34
36
  private options;
37
+ private methods;
35
38
  constructor(files: LambderFiles, options: LambderPublicFilesOptions);
36
39
  /** Serve the mapped file, or return null to fall through. */
37
40
  handle(ctx: LambderRenderContext): Promise<LambderResponse | null>;
@@ -1,4 +1,5 @@
1
1
  import { LambderResponse } from "./LambderResponse.js";
2
+ import { allowsRequestMethod } from "./LambderRouting.js";
2
3
  // Content-hashed build outputs (Vite/webpack/Rollup): a [-.] separated run of
3
4
  // 8+ hash chars containing at least one digit, before the extension.
4
5
  const DEFAULT_IMMUTABLE_PATTERN = /[-.](?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/;
@@ -7,20 +8,24 @@ const DEFAULT_CACHE_CONTROL = "public, max-age=3600";
7
8
  /**
8
9
  * Terminal public-file handler registered via lambder.servePublicFiles().
9
10
  * Runs only when no route matched, so it can never shadow routes registered
10
- * after it. Serves files through the instance's reader (traversal-safe,
11
+ * after it. Serves files through the instance's reader (one path rule,
11
12
  * mime-typed, memory-cached) with the immutable-cache heuristic for
12
13
  * content-hashed assets, and falls through to the route fallback when the
13
- * source has no such file.
14
+ * method is not configured or the source has no such file.
14
15
  */
15
16
  export class LambderPublicFilesHandler {
16
17
  files;
17
18
  options;
19
+ methods;
18
20
  constructor(files, options) {
19
21
  this.files = files;
20
22
  this.options = options;
23
+ this.methods = new Set((options.methods ?? ["GET", "HEAD"]).map((method) => method.toUpperCase()));
21
24
  }
22
25
  /** Serve the mapped file, or return null to fall through. */
23
26
  async handle(ctx) {
27
+ if (!allowsRequestMethod(this.methods, ctx.method))
28
+ return null;
24
29
  const mappedPath = this.options.path ? this.options.path(ctx) : ctx.path;
25
30
  if (!mappedPath)
26
31
  return null;
@@ -1,8 +1,10 @@
1
- import LambderResponseBuilder, { type LambderApiAnswer, type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
1
+ import type { LambderApiNullAnswerConfig } from "../shared/wire/LambderApiContract.js";
2
+ import LambderResponseBuilder, { type LambderResolverApiMethod, type LambderApiResponseConfig, type LambderResponseOptions } from "./LambderResponseBuilder.js";
2
3
  import type { LambderResponse } from "./LambderResponse.js";
3
4
  type SyncDie<T extends (...args: any[]) => LambderResponse> = (...args: Parameters<T>) => never;
4
5
  type AsyncDie<T extends (...args: any[]) => Promise<LambderResponse>> = (...args: Parameters<T>) => Promise<never>;
5
- export interface DieResolverMethods<TOutput> {
6
+ /** The `res.die.*` surface: every builder method, throwing what it built. Internal to the resolver, which is the only thing that has one. */
7
+ interface DieResolverMethods<TOutput> {
6
8
  raw: SyncDie<LambderResponseBuilder["raw"]>;
7
9
  json: SyncDie<LambderResponseBuilder["json"]>;
8
10
  text: SyncDie<LambderResponseBuilder["text"]>;
@@ -13,8 +15,8 @@ export interface DieResolverMethods<TOutput> {
13
15
  redirect: SyncDie<LambderResponseBuilder["redirect"]>;
14
16
  versionExpired: SyncDie<LambderResponseBuilder["versionExpired"]>;
15
17
  fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
16
- api: LambderApiAnswer<TOutput, never>;
17
- apiBinary: LambderApiAnswer<TOutput, never>;
18
+ api: LambderResolverApiMethod<TOutput, never>;
19
+ apiBinary: LambderResolverApiMethod<TOutput, never>;
18
20
  file: AsyncDie<LambderResponseBuilder["file"]>;
19
21
  templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
20
22
  }
@@ -30,8 +32,8 @@ export default class LambderResolver<TOutput = any> extends LambderResponseBuild
30
32
  die: DieResolverMethods<TOutput>;
31
33
  constructor(...args: ConstructorParameters<typeof LambderResponseBuilder>);
32
34
  api(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
33
- api(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
35
+ api(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
34
36
  apiBinary(payload: TOutput, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
35
- apiBinary(payload: null, config: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
37
+ apiBinary(payload: null, config: LambderApiNullAnswerConfig, options?: LambderResponseOptions): LambderResponse;
36
38
  }
37
39
  export {};
@@ -1,6 +1,8 @@
1
- import type { LambderCompressionOption, LambderCompressionSettingsBase, LambderEncoding } from "../shared/LambderCompressionOption.js";
2
- import type { LambderRenderContext } from "./LambderContext.js";
3
- export type HttpStatusCode = 100 | 101 | 200 | 201 | 202 | 203 | 204 | 206 | 300 | 301 | 302 | 303 | 304 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 408 | 409 | 410 | 412 | 413 | 415 | 416 | 418 | 422 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504;
1
+ import { LAMBDER_RESPONSE_BRAND } from "../shared/util/LambderResponseBrand.js";
2
+ import type { LambderCompressionOption, LambderCompressionSettingsBase, LambderEncoding } from "../shared/wire/LambderCompressionOption.js";
3
+ import type { LambderRenderContext, LambderHttpEventFormat } from "./LambderContext.js";
4
+ import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
5
+ import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
4
6
  export type LambderHeadersInput = Record<string, string | string[]>;
5
7
  /**
6
8
  * Final Lambda response: v1 (REST API) uses multiValueHeaders, v2 (HTTP API /
@@ -18,10 +20,9 @@ export type LambderHttpResponse = {
18
20
  headers?: Record<string, string>;
19
21
  cookies?: string[];
20
22
  };
21
- export type LambderHttpEventFormat = "v1" | "v2";
22
- export declare const normalizeHeaders: (headers?: LambderHeadersInput) => Record<string, string[]>;
23
- export type LambderResponseInit = {
24
- statusCode: HttpStatusCode;
23
+ /** The constructor argument of LambderResponse; built through the resolver rather than by hand, so it is internal to this module. */
24
+ type LambderResponseInit = {
25
+ statusCode: LambderHttpStatusCode;
25
26
  headers?: LambderHeadersInput;
26
27
  body?: string | Buffer | null;
27
28
  /** True when body is already a base64-encoded string (pre-encoded binary content). */
@@ -41,7 +42,8 @@ export type LambderResponseInit = {
41
42
  * the request: the thrown response becomes the response.
42
43
  */
43
44
  export declare class LambderResponse {
44
- statusCode: HttpStatusCode;
45
+ readonly [LAMBDER_RESPONSE_BRAND]: true;
46
+ statusCode: LambderHttpStatusCode;
45
47
  headers: Record<string, string[]>;
46
48
  body: string | Buffer | null;
47
49
  isBodyBase64: boolean;
@@ -52,8 +54,16 @@ export declare class LambderResponse {
52
54
  setHeader(key: string, value: string | string[]): this;
53
55
  addHeader(key: string, value: string): this;
54
56
  }
55
- export declare const isCompressibleContentType: (contentType: string | undefined) => boolean;
56
- export declare const acceptsEncoding: (acceptEncoding: string | undefined | null, encoding: string) => boolean;
57
+ /**
58
+ * A handler's response as a core answer: what the API pipeline stores,
59
+ * replays and hands back. A Buffer body travels base64-encoded and marked
60
+ * as such, so the idempotency engine never caches it and finalization
61
+ * passes it through untouched; the compress and etag flags ride along so
62
+ * nothing a handler asked for is lost on the way through the core.
63
+ */
64
+ export declare const answerFromResponse: (response: LambderResponse) => LambderApiAnswer;
65
+ /** A core answer as the response hooks, CORS and finalization work on. */
66
+ export declare const responseFromAnswer: (answer: LambderApiAnswer) => LambderResponse;
57
67
  /** Response-side settings: the threshold plus what the wire can negotiate. */
58
68
  export type LambderResponseCompressionSettings = LambderCompressionSettingsBase & {
59
69
  /** Preference order; the first the client accepts wins. */
@@ -80,10 +90,18 @@ export type LambderFinalizeOptions = {
80
90
  */
81
91
  export declare const DEFAULT_RESPONSE_COMPRESSION_SETTINGS: LambderResponseCompressionSettings;
82
92
  export declare const DEFAULT_FINALIZE_OPTIONS: LambderFinalizeOptions;
93
+ /**
94
+ * Emit the format-specific Lambda response shape. Exported because the
95
+ * last-resort crash path has to emit without finalizing (finalization may be
96
+ * what failed) and must still get the shape right; hand-writing it there left
97
+ * the v1/v2 split in four places.
98
+ */
99
+ export declare const emitResponse: (format: LambderHttpEventFormat, statusCode: number, headers: Record<string, string[]>, body: string, isBase64Encoded: boolean) => LambderHttpResponse;
83
100
  /**
84
101
  * Convert an intermediate LambderResponse into the final Lambda response:
85
102
  * gzip negotiation (Accept-Encoding), ETag + If-None-Match 304, base64
86
103
  * encoding, HEAD body stripping, and Lambda payload size guard. Emits the v1
87
104
  * (REST API) or v2 (HTTP API / Function URL) response shape.
88
105
  */
89
- export declare const finalizeResponse: (ctx: Pick<LambderRenderContext, "method" | "headers"> | null, response: LambderResponse, options: LambderFinalizeOptions, format?: LambderHttpEventFormat) => Promise<LambderHttpResponse>;
106
+ export declare const finalizeResponse: (ctx: Pick<LambderRenderContext, "method" | "header"> | null, response: LambderResponse, options: LambderFinalizeOptions, format?: LambderHttpEventFormat) => Promise<LambderHttpResponse>;
107
+ export {};