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.
- package/CHANGELOG.md +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- 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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
63
|
-
*
|
|
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
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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)}${
|
|
11
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
/**
|
|
78
|
-
|
|
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
|
|
97
|
-
* the
|
|
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
|
-
*
|
|
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 {};
|