lambder 7.2.5 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -1,5 +1,6 @@
1
1
  import { isV2HttpEvent } from "./LambderContext.js";
2
2
  import { allowsRequestMethod } from "./LambderRouting.js";
3
+ import { encodePathForLocation } from "./LambderRequestPath.js";
3
4
  /**
4
5
  * Serves the app shell for page requests that nothing else handled,
5
6
  * registered via lambder.serveIndexHtml(). Runs after servePublicFiles in the
@@ -26,7 +27,7 @@ export class LambderIndexHtmlHandler {
26
27
  if (options.redirectTrailingSlash && ctx.path.length > 1 && ctx.path.endsWith("/")) {
27
28
  const target = sameOriginRedirectTarget(ctx.path);
28
29
  if (target !== null)
29
- return resolver.redirect(target + buildQueryString(ctx), 301);
30
+ return resolver.redirect(encodePathForLocation(target) + buildQueryString(ctx), 301);
30
31
  }
31
32
  const response = handler
32
33
  ? await handler(ctx, resolver)
@@ -42,13 +43,14 @@ export class LambderIndexHtmlHandler {
42
43
  * cannot leave this origin, or null when no such form exists.
43
44
  *
44
45
  * `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.
46
+ * navigates to that host, and browsers normalize backslashes into slashes, so
47
+ * `/\evil.example` is the same thing. The header is built from the request
48
+ * path, which the caller writes, so without this `GET //evil.example/` would
49
+ * make redirectTrailingSlash an open redirect. The leading run of slashes and
50
+ * backslashes collapses to one slash, and the result is then checked rather
51
+ * than assumed, since the header's safety rests on that check. The caller
52
+ * then percent-encodes it (encodePathForLocation), so a TAB or line break,
53
+ * which a browser drops, cannot make a second slash out of it.
52
54
  */
53
55
  const sameOriginRedirectTarget = (path) => {
54
56
  const target = path.replace(/^[/\\]+/, "/").replace(/[/\\]+$/, "") || "/";
@@ -59,9 +61,8 @@ const sameOriginRedirectTarget = (path) => {
59
61
  /**
60
62
  * Rebuild the query string from the API Gateway event for redirects.
61
63
  *
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.
64
+ * From the raw event rather than ctx.get, which keeps one value per key and
65
+ * loses their order; a redirect has to hand back the query it was given.
65
66
  */
66
67
  const buildQueryString = (ctx) => {
67
68
  if (isV2HttpEvent(ctx.event)) {
@@ -2,11 +2,10 @@
2
2
  * The server's policy builders: the generic builders from `api/`, bound to
3
3
  * the render contexts a Lambda handler runs on.
4
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.
5
+ * They live in `core/` rather than beside the engines because the binding is
6
+ * the only server-specific part of a guard or a rate-limit key, and keeping it
7
+ * here means `api/` never imports from `core/`. The mock runtime binds the
8
+ * same generic builders to its own call contexts in its own layer.
10
9
  */
11
10
  import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
12
11
  import { type LambderGuardBuilder } from "../api/LambderApiGuards.js";
@@ -15,3 +14,16 @@ import { type LambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js"
15
14
  export declare const lambderGuard: LambderGuardBuilder<LambderRenderContext, LambderSessionRenderContext<any, any>>;
16
15
  /** Builder for the server's rate-limit keys: the handler sees the render context. */
17
16
  export declare const lambderRateLimitKey: LambderRateLimitKeyBuilder<LambderRenderContext>;
17
+ /**
18
+ * The same two builders bound to one app's session data, which is what
19
+ * initLambder<SessionData>() hands out beside create(): a guard's
20
+ * `ctx.session.data` and `ctx.sessionController` are SessionData where the standalone
21
+ * lambderGuard() leaves them `any`. The server's counterpart of the mock's
22
+ * `guard` and `rateLimitKey`.
23
+ */
24
+ export declare const policyBuildersFor: <TSessionData>() => {
25
+ /** Builds a guard whose handler sees this app's session type. */
26
+ guard: LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>>;
27
+ /** Builds a rate-limit key whose handler sees this app's session type; the counterpart of `guard`. */
28
+ rateLimitKey: LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
29
+ };
@@ -2,11 +2,10 @@
2
2
  * The server's policy builders: the generic builders from `api/`, bound to
3
3
  * the render contexts a Lambda handler runs on.
4
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.
5
+ * They live in `core/` rather than beside the engines because the binding is
6
+ * the only server-specific part of a guard or a rate-limit key, and keeping it
7
+ * here means `api/` never imports from `core/`. The mock runtime binds the
8
+ * same generic builders to its own call contexts in its own layer.
10
9
  */
11
10
  import { lambderGuardBuilder } from "../api/LambderApiGuards.js";
12
11
  import { lambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
@@ -14,3 +13,16 @@ import { lambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
14
13
  export const lambderGuard = lambderGuardBuilder();
15
14
  /** Builder for the server's rate-limit keys: the handler sees the render context. */
16
15
  export const lambderRateLimitKey = lambderRateLimitKeyBuilder();
16
+ /**
17
+ * The same two builders bound to one app's session data, which is what
18
+ * initLambder<SessionData>() hands out beside create(): a guard's
19
+ * `ctx.session.data` and `ctx.sessionController` are SessionData where the standalone
20
+ * lambderGuard() leaves them `any`. The server's counterpart of the mock's
21
+ * `guard` and `rateLimitKey`.
22
+ */
23
+ export const policyBuildersFor = () => ({
24
+ /** Builds a guard whose handler sees this app's session type. */
25
+ guard: lambderGuardBuilder(),
26
+ /** Builds a rate-limit key whose handler sees this app's session type; the counterpart of `guard`. */
27
+ rateLimitKey: lambderRateLimitKeyBuilder(),
28
+ });
@@ -7,15 +7,21 @@ export type LambderPublicFilesOptions = {
7
7
  methods?: string[];
8
8
  /**
9
9
  * Map the request to a file path (app-owned logic, e.g. per-tenant
10
- * roots: (ctx) => `${brand(ctx.host)}${ctx.path}`). Return
11
- * null/undefined to skip. Default: (ctx) => ctx.path.
10
+ * roots: (ctx, filePath) => `${brand(ctx.host)}${filePath}`). `filePath`
11
+ * is the file ctx.path names, its kept `%25` read as `%`; a path with an
12
+ * encoded slash inside a segment names no file and never reaches the
13
+ * mapper. Return null/undefined to skip. Default: the file path as it is.
12
14
  */
13
- path?: (ctx: LambderRenderContext) => string | null | undefined;
15
+ path?: (ctx: LambderRenderContext, filePath: string) => string | null | undefined;
14
16
  /** Cache-Control for served files; the function receives the relative file path. Default: "public, max-age=3600". */
15
17
  cacheControl?: string | ((ctx: LambderRenderContext, relativePath: string) => string);
16
- /** Filenames matching this get immutableCacheControl. Default: content-hash heuristic. Set false to disable. */
18
+ /** Relative paths matching this get immutableCacheControl. Default: content-hashed names in a bundler's output folder (assets/, static/, _next/static/). Set false to disable. */
17
19
  immutablePattern?: RegExp | false;
18
- /** Default: "public, max-age=31536000, immutable". */
20
+ /**
21
+ * Default: "public, max-age=31536000, immutable". Like any Cache-Control,
22
+ * it goes out private, without `immutable`, on an answer that also sets a
23
+ * cookie (a hook's guest session, a slid session cookie): see emitResponse.
24
+ */
19
25
  immutableCacheControl?: string;
20
26
  /**
21
27
  * Compression per file: "auto" (default: compressible mime + size threshold),
@@ -1,8 +1,33 @@
1
1
  import { LambderResponse } from "./LambderResponse.js";
2
2
  import { allowsRequestMethod } from "./LambderRouting.js";
3
- // Content-hashed build outputs (Vite/webpack/Rollup): a [-.] separated run of
4
- // 8+ hash chars containing at least one digit, before the extension.
5
- const DEFAULT_IMMUTABLE_PATTERN = /[-.](?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/;
3
+ import { filePathOf } from "./LambderRequestPath.js";
4
+ /*
5
+ * Build outputs whose names carry a content hash, so a changed file gets a
6
+ * new name and the old one may be cached for good. Only what a bundler wrote:
7
+ * - everything under Next.js's _next/static/;
8
+ * - under assets/ or static/ (Vite, Rollup, esbuild, webpack and CRA, a
9
+ * Django manifest), a name ending in its hash, then an optional `.chunk`
10
+ * and the extension. The hash is either exactly 8 base64url characters
11
+ * after a hyphen (Vite, Rollup, esbuild), or 8 or more letters and
12
+ * digits with both kinds among them (webpack's contenthash, after a dot
13
+ * or a hyphen).
14
+ * The 8-character form has to look random, since a hand-named file's last
15
+ * word is often 8 characters too: a capital, a lowercase letter and a digit,
16
+ * or, with no digit, at least three capitals and a lowercase letter. A word
17
+ * in PascalCase (Inter-SemiBold.woff2, icon-Settings.svg) has fewer capitals
18
+ * than that, and lowercase words around a version (og-image-v2-final.png)
19
+ * have none. A hyphen may sit inside the 8, as Rollup's hashes put one
20
+ * there. The lookaheads cannot read past the 8, since a dot follows them.
21
+ * About one real hash in twenty fails the test (one without a digit and
22
+ * with fewer than three capitals) and is served with the ordinary
23
+ * Cache-Control: a revalidation, never a stale file.
24
+ * A hand-named file (android-chrome-192x192.png, team-photo-2023.jpg,
25
+ * privacy-policy-v2.html) keeps the ordinary Cache-Control: marked immutable,
26
+ * a replaced copy would never reach a browser that already had it.
27
+ */
28
+ const EXACT_BUNDLER_HASH = "-(?=[\\w-]{0,7}[a-z])(?:(?=[\\w-]{0,7}[A-Z])(?=[\\w-]{0,7}[0-9])|(?=(?:[a-z0-9_-]*[A-Z]){3}))[\\w-]{8}";
29
+ const LONG_BUNDLER_HASH = "[-.](?=\\w*[A-Za-z])(?=\\w*\\d)\\w{8,}";
30
+ const DEFAULT_IMMUTABLE_PATTERN = new RegExp(`(?:^|/)_next/static/|(?:^|/)(?:assets|static)/(?:[^/]+/)*[^/]*(?:${EXACT_BUNDLER_HASH}|${LONG_BUNDLER_HASH})(?:\\.chunk)?\\.[A-Za-z0-9]+$`);
6
31
  const DEFAULT_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
7
32
  const DEFAULT_CACHE_CONTROL = "public, max-age=3600";
8
33
  /**
@@ -26,7 +51,10 @@ export class LambderPublicFilesHandler {
26
51
  async handle(ctx) {
27
52
  if (!allowsRequestMethod(this.methods, ctx.method))
28
53
  return null;
29
- const mappedPath = this.options.path ? this.options.path(ctx) : ctx.path;
54
+ const filePath = filePathOf(ctx.path);
55
+ if (filePath === null)
56
+ return null;
57
+ const mappedPath = this.options.path ? this.options.path(ctx, filePath) : filePath;
30
58
  if (!mappedPath)
31
59
  return null;
32
60
  const file = await this.files.read(mappedPath);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A request path between the spelling a gateway delivers and the one routes,
3
+ * files and redirects work with.
4
+ *
5
+ * The gateways disagree: a REST API and a Function URL hand the path over
6
+ * percent-encoded, an HTTP API hands it over decoded. Routes and file
7
+ * lookups need one form, or `addRoute("/hakkımızda")` and a file named
8
+ * `team photo.jpg` are found on one gateway and not on another.
9
+ *
10
+ * That form, `ctx.path`, is the path decoded with two escapes kept: a slash
11
+ * inside a segment stays `%2F`, so it cannot become a separator, and a
12
+ * percent sign stays `%25`, so no decoded text can pass for an escape. Every
13
+ * path is decoded exactly once, whoever sent it: `/%2561dmin` names the text
14
+ * `%61dmin` (written `/%2561dmin`), and never `/admin`.
15
+ */
16
+ /**
17
+ * The path routes and files see (`ctx.path`), from the path the gateway
18
+ * delivered. `alreadyDecoded` for an HTTP API, which decodes the path before
19
+ * the function sees it (its encoded slashes into separators, too): decoded
20
+ * again, a `/%2561dmin` the gateway handed over as `/%61dmin` would route to
21
+ * `/admin`, past an authorizer, a WAF rule or a CloudFront behavior in front
22
+ * of the function that checked the path once. The segment checks that guard
23
+ * file reads (no `..`, no empty or backslashed segment) run on this form, so
24
+ * an encoded `%2e%2e` is refused as the `..` it is.
25
+ */
26
+ export declare const decodeRequestPath: (rawPath: string, alreadyDecoded: boolean) => string;
27
+ /** A path parameter's value: its text from ctx.path, with the kept `%2F` and `%25` turned back into `/` and `%`. */
28
+ export declare const decodePathParam: (value: string) => string;
29
+ /**
30
+ * The file a path names, for servePublicFiles: the kept `%25` turned back
31
+ * into `%`, or null for a path with a slash inside a segment, which no file
32
+ * name holds.
33
+ */
34
+ export declare const filePathOf: (path: string) => string | null;
35
+ /**
36
+ * A decoded path as a Location header: every character a URL path may not
37
+ * carry is percent-encoded, `%` itself left alone so the kept `%2F` and `%25`
38
+ * stay the escapes they are. A browser drops a TAB or line break inside a
39
+ * Location before resolving it, so `/<TAB>/evil.example` sent as it is would
40
+ * reach the browser as `//evil.example`, another host; encoded, it stays a
41
+ * path on this one.
42
+ */
43
+ export declare const encodePathForLocation: (path: string) => string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A request path between the spelling a gateway delivers and the one routes,
3
+ * files and redirects work with.
4
+ *
5
+ * The gateways disagree: a REST API and a Function URL hand the path over
6
+ * percent-encoded, an HTTP API hands it over decoded. Routes and file
7
+ * lookups need one form, or `addRoute("/hakkımızda")` and a file named
8
+ * `team photo.jpg` are found on one gateway and not on another.
9
+ *
10
+ * That form, `ctx.path`, is the path decoded with two escapes kept: a slash
11
+ * inside a segment stays `%2F`, so it cannot become a separator, and a
12
+ * percent sign stays `%25`, so no decoded text can pass for an escape. Every
13
+ * path is decoded exactly once, whoever sent it: `/%2561dmin` names the text
14
+ * `%61dmin` (written `/%2561dmin`), and never `/admin`.
15
+ */
16
+ /** An encoded slash: a slash that belongs to a segment's text rather than separating two segments. */
17
+ const ENCODED_SLASH = /%2F/gi;
18
+ /** Decoded text as ctx.path writes it: every percent sign kept as `%25`. */
19
+ const keepPercentSigns = (text) => text.replaceAll("%", "%25");
20
+ /**
21
+ * One segment decoded, its encoded slashes kept as `%2F` and its percent
22
+ * signs as `%25`. A segment whose escapes do not decode (a stray `%`, a byte
23
+ * sequence that is not UTF-8) is read as the text it arrived as: it names
24
+ * nothing that decoding would have named.
25
+ */
26
+ const decodeSegment = (segment) => {
27
+ if (!segment.includes("%"))
28
+ return segment;
29
+ try {
30
+ return segment.split(ENCODED_SLASH).map((piece) => keepPercentSigns(decodeURIComponent(piece))).join("%2F");
31
+ }
32
+ catch {
33
+ return keepPercentSigns(segment);
34
+ }
35
+ };
36
+ /**
37
+ * The path routes and files see (`ctx.path`), from the path the gateway
38
+ * delivered. `alreadyDecoded` for an HTTP API, which decodes the path before
39
+ * the function sees it (its encoded slashes into separators, too): decoded
40
+ * again, a `/%2561dmin` the gateway handed over as `/%61dmin` would route to
41
+ * `/admin`, past an authorizer, a WAF rule or a CloudFront behavior in front
42
+ * of the function that checked the path once. The segment checks that guard
43
+ * file reads (no `..`, no empty or backslashed segment) run on this form, so
44
+ * an encoded `%2e%2e` is refused as the `..` it is.
45
+ */
46
+ export const decodeRequestPath = (rawPath, alreadyDecoded) => rawPath.split("/").map(alreadyDecoded ? keepPercentSigns : decodeSegment).join("/");
47
+ /** A path parameter's value: its text from ctx.path, with the kept `%2F` and `%25` turned back into `/` and `%`. */
48
+ export const decodePathParam = (value) => value.replace(/%2F|%25/gi, (escape) => (escape === "%25" ? "%" : "/"));
49
+ /**
50
+ * The file a path names, for servePublicFiles: the kept `%25` turned back
51
+ * into `%`, or null for a path with a slash inside a segment, which no file
52
+ * name holds.
53
+ */
54
+ export const filePathOf = (path) => /%2F/i.test(path) ? null : path.replaceAll("%25", "%");
55
+ /**
56
+ * A decoded path as a Location header: every character a URL path may not
57
+ * carry is percent-encoded, `%` itself left alone so the kept `%2F` and `%25`
58
+ * stay the escapes they are. A browser drops a TAB or line break inside a
59
+ * Location before resolving it, so `/<TAB>/evil.example` sent as it is would
60
+ * reach the browser as `//evil.example`, another host; encoded, it stays a
61
+ * path on this one.
62
+ */
63
+ export const encodePathForLocation = (path) => path.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]/gu, (character) => encodeURIComponent(character));
@@ -50,6 +50,14 @@ export declare class LambderResponse {
50
50
  compress: boolean | "auto";
51
51
  etag: boolean | "auto";
52
52
  constructor(init: LambderResponseInit);
53
+ /**
54
+ * A copy with its own header lists, for a request to write into. A
55
+ * handler may answer with an object it keeps between requests (a
56
+ * module-level 404), and everything downstream adds headers (cookies,
57
+ * CORS, Vary, Content-Encoding, ETag), which would carry one caller's
58
+ * Set-Cookie to the next. The body is shared: nothing writes into it.
59
+ */
60
+ copy(): LambderResponse;
53
61
  getHeader(key: string): string[] | undefined;
54
62
  setHeader(key: string, value: string | string[]): this;
55
63
  addHeader(key: string, value: string): this;
@@ -74,8 +82,14 @@ export type LambderResponseCompressionSettings = LambderCompressionSettingsBase
74
82
  /** The `compression` option at creation: `true` for the defaults, `false` for off, or overrides. */
75
83
  export type LambderResponseCompressionOption = LambderCompressionOption<LambderResponseCompressionSettings>;
76
84
  export type LambderFinalizeOptions = {
77
- /** Resolved settings, or null when compression is off: the same `Settings | null` contract the stores hold. */
78
- compression: LambderResponseCompressionSettings | null;
85
+ /**
86
+ * Resolved settings per event format, or null where compression is off:
87
+ * the same `Settings | null` contract the stores hold. Per format because
88
+ * a REST API (v1) hands a compressed body on to the browser only when its
89
+ * binaryMediaTypes match, which in practice means `*\/*`: compressing
90
+ * there by default would send most answers as base64 text.
91
+ */
92
+ compression: Record<LambderHttpEventFormat, LambderResponseCompressionSettings | null>;
79
93
  etag: boolean;
80
94
  /** Guard against Lambda's ~6MB response cap with a clear error. */
81
95
  maxResponseBytes: number;
@@ -93,15 +107,22 @@ export declare const DEFAULT_FINALIZE_OPTIONS: LambderFinalizeOptions;
93
107
  /**
94
108
  * Emit the format-specific Lambda response shape. Exported because the
95
109
  * 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.
110
+ * what failed) and must still get the shape right, so the v1/v2 split lives
111
+ * in this one place. Being the one exit every answer leaves through (each of
112
+ * finalization's, the 304 included, and the crash path's), it is also where
113
+ * an answer that sets a cookie is made private (privateWhenSettingCookies).
98
114
  */
99
115
  export declare const emitResponse: (format: LambderHttpEventFormat, statusCode: number, headers: Record<string, string[]>, body: string, isBase64Encoded: boolean) => LambderHttpResponse;
100
116
  /**
101
117
  * Convert an intermediate LambderResponse into the final Lambda response:
102
- * gzip negotiation (Accept-Encoding), ETag + If-None-Match 304, base64
118
+ * compression negotiation (Accept-Encoding), ETag + If-None-Match 304, base64
103
119
  * encoding, HEAD body stripping, and Lambda payload size guard. Emits the v1
104
120
  * (REST API) or v2 (HTTP API / Function URL) response shape.
121
+ *
122
+ * Text goes out as text and only bytes as base64: a REST API decodes base64
123
+ * only for its binaryMediaTypes, so a stylesheet sent as base64 would reach
124
+ * the browser as base64. The ETag is settled before anything is compressed, so
125
+ * a revalidation that ends in a 304 compresses nothing.
105
126
  */
106
127
  export declare const finalizeResponse: (ctx: Pick<LambderRenderContext, "method" | "header"> | null, response: LambderResponse, options: LambderFinalizeOptions, format?: LambderHttpEventFormat) => Promise<LambderHttpResponse>;
107
128
  export {};