lambder 7.3.1 → 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 +933 -3
- package/README.md +41 -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 +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -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 +140 -75
- package/dist/core/Lambder.js +347 -227
- 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 +21 -7
- package/dist/core/LambderFiles.js +62 -34
- 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 +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- 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 +1 -1
- package/dist/mock.js +2 -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 +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- 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/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 +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- 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 +21 -23
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +12 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -13,20 +13,36 @@ export declare const LOOPBACK_CLIENT_IP = "127.0.0.1";
|
|
|
13
13
|
/**
|
|
14
14
|
* One textual form per address, so a rate-limit counter cannot be split.
|
|
15
15
|
* A forwarding proxy may write the RFC 7239 bracket-and-port form
|
|
16
|
-
* ([2001:db8::1]:443) or a plain host:port
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* is
|
|
16
|
+
* ([2001:db8::1]:443) or a plain host:port; each variant would otherwise be
|
|
17
|
+
* its own counter, which is a limit that does not limit. An unbracketed IPv6
|
|
18
|
+
* address with a port after it cannot be told apart by its text
|
|
19
|
+
* (`2001:db8::1:443` is an address too), so this leaves it alone; the header
|
|
20
|
+
* that writes that form has its port taken off by resolveClientIp, which
|
|
21
|
+
* knows where the value came from. Anything longer than the longest valid
|
|
22
|
+
* address is truncated rather than trusted as a key.
|
|
20
23
|
*/
|
|
21
24
|
export declare const normalizeClientIp: (value: string) => string;
|
|
25
|
+
/** The IPv6 prefix a per-IP rate limit counts by default: the /64 every subscriber holds at least. */
|
|
26
|
+
export declare const DEFAULT_IPV6_RATE_LIMIT_PREFIX = 64;
|
|
27
|
+
/**
|
|
28
|
+
* The caller a per-IP rate limit counts, which is not always the address
|
|
29
|
+
* itself. An IPv4 address is one subscriber. An IPv6 subscriber, a VPS
|
|
30
|
+
* included, holds at least a /64 and may pick any interface id inside it, so
|
|
31
|
+
* one counter per full address is a fresh counter per request for anyone
|
|
32
|
+
* who rotates: the prefix is what is counted (`2001:db8:1:2:0:0:0:0/64`). An
|
|
33
|
+
* IPv4-mapped IPv6 address (::ffff:192.0.2.1) is its IPv4 address, so the
|
|
34
|
+
* two spellings share a counter. A value that is not an address is counted
|
|
35
|
+
* as it is. ctx.ip itself stays the exact address, for logs.
|
|
36
|
+
*/
|
|
37
|
+
export declare const rateLimitSubjectOf: (ip: string, ipv6PrefixLength?: number) => string;
|
|
22
38
|
/**
|
|
23
39
|
* The first trusted header that carries anything, leftmost entry, else the
|
|
24
|
-
* address the gateway observed
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* from anyway.
|
|
40
|
+
* address the gateway observed; a header that always appends the port (see
|
|
41
|
+
* PORT_SUFFIXED_IP_HEADERS) has it taken off. Nothing is trusted by default,
|
|
42
|
+
* and there is no exception for an invoke: the invoke marker is an ordinary
|
|
43
|
+
* request header any HTTP caller can set, so honouring it would let every
|
|
44
|
+
* caller choose its own address. A genuine invoke needs no exception,
|
|
45
|
+
* because the synthesized event carries the end user's address in
|
|
46
|
+
* requestContext.http.sourceIp, which is where `sourceIp` comes from anyway.
|
|
31
47
|
*/
|
|
32
48
|
export declare const resolveClientIp: (lowercasedHeaders: Record<string, string>, sourceIp: string, trustedClientIpHeaders?: readonly string[]) => string;
|
|
@@ -12,13 +12,60 @@
|
|
|
12
12
|
export const LOOPBACK_CLIENT_IP = "127.0.0.1";
|
|
13
13
|
/** Longest textual IPv6 address (39) plus a scope id; beyond this the value is not an address. */
|
|
14
14
|
const MAX_CLIENT_IP_LENGTH = 45;
|
|
15
|
+
/** A dotted IPv4 address as its four octets, or null when the text is not one. */
|
|
16
|
+
const parseIpv4 = (value) => {
|
|
17
|
+
const parts = value.split(".");
|
|
18
|
+
if (parts.length !== 4 || !parts.every((part) => /^\d{1,3}$/.test(part)))
|
|
19
|
+
return null;
|
|
20
|
+
const octets = parts.map(Number);
|
|
21
|
+
return octets.every((octet) => octet <= 255) ? octets : null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* An IPv6 address as its eight 16-bit groups, or null when the text is not
|
|
25
|
+
* one: every spelling of one address (compressed or not, any case, with an
|
|
26
|
+
* embedded IPv4 tail) parses to the same groups.
|
|
27
|
+
*/
|
|
28
|
+
const parseIpv6 = (value) => {
|
|
29
|
+
// A zone id names the interface an address was reached on, not the address.
|
|
30
|
+
let text = value.split("%")[0];
|
|
31
|
+
const lastColon = text.lastIndexOf(":");
|
|
32
|
+
if (lastColon === -1)
|
|
33
|
+
return null;
|
|
34
|
+
const tail = text.slice(lastColon + 1);
|
|
35
|
+
if (tail.includes(".")) {
|
|
36
|
+
const octets = parseIpv4(tail);
|
|
37
|
+
if (!octets)
|
|
38
|
+
return null;
|
|
39
|
+
text = `${text.slice(0, lastColon + 1)}${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
|
|
40
|
+
}
|
|
41
|
+
const halves = text.split("::");
|
|
42
|
+
if (halves.length > 2)
|
|
43
|
+
return null;
|
|
44
|
+
const groupsOf = (part) => {
|
|
45
|
+
if (part === "")
|
|
46
|
+
return [];
|
|
47
|
+
const groups = part.split(":");
|
|
48
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/i.test(group)) ? groups.map((group) => parseInt(group, 16)) : null;
|
|
49
|
+
};
|
|
50
|
+
const head = groupsOf(halves[0]);
|
|
51
|
+
const rest = halves.length === 2 ? groupsOf(halves[1]) : [];
|
|
52
|
+
if (!head || !rest)
|
|
53
|
+
return null;
|
|
54
|
+
if (halves.length === 1)
|
|
55
|
+
return head.length === 8 ? head : null;
|
|
56
|
+
const missing = 8 - head.length - rest.length;
|
|
57
|
+
return missing >= 1 ? [...head, ...new Array(missing).fill(0), ...rest] : null;
|
|
58
|
+
};
|
|
15
59
|
/**
|
|
16
60
|
* One textual form per address, so a rate-limit counter cannot be split.
|
|
17
61
|
* A forwarding proxy may write the RFC 7239 bracket-and-port form
|
|
18
|
-
* ([2001:db8::1]:443) or a plain host:port
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* is
|
|
62
|
+
* ([2001:db8::1]:443) or a plain host:port; each variant would otherwise be
|
|
63
|
+
* its own counter, which is a limit that does not limit. An unbracketed IPv6
|
|
64
|
+
* address with a port after it cannot be told apart by its text
|
|
65
|
+
* (`2001:db8::1:443` is an address too), so this leaves it alone; the header
|
|
66
|
+
* that writes that form has its port taken off by resolveClientIp, which
|
|
67
|
+
* knows where the value came from. Anything longer than the longest valid
|
|
68
|
+
* address is truncated rather than trusted as a key.
|
|
22
69
|
*/
|
|
23
70
|
export const normalizeClientIp = (value) => {
|
|
24
71
|
let ip = value.trim();
|
|
@@ -35,22 +82,58 @@ export const normalizeClientIp = (value) => {
|
|
|
35
82
|
}
|
|
36
83
|
return ip.toLowerCase().slice(0, MAX_CLIENT_IP_LENGTH);
|
|
37
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Headers whose value always ends in `:port`, whatever the address before
|
|
87
|
+
* it: CloudFront-Viewer-Address writes `192.0.2.1:443` and
|
|
88
|
+
* `2001:db8::1:443` alike, with no brackets. The last colon-separated
|
|
89
|
+
* segment of such a value is the port by definition. Read from the text
|
|
90
|
+
* instead, a compressed IPv6 address with its port on still parses, the port
|
|
91
|
+
* becomes its last group, and the /64 a per-IP limit counts moves with the
|
|
92
|
+
* interface id the caller picks.
|
|
93
|
+
*/
|
|
94
|
+
const PORT_SUFFIXED_IP_HEADERS = new Set(["cloudfront-viewer-address"]);
|
|
95
|
+
/** The IPv6 prefix a per-IP rate limit counts by default: the /64 every subscriber holds at least. */
|
|
96
|
+
export const DEFAULT_IPV6_RATE_LIMIT_PREFIX = 64;
|
|
97
|
+
/**
|
|
98
|
+
* The caller a per-IP rate limit counts, which is not always the address
|
|
99
|
+
* itself. An IPv4 address is one subscriber. An IPv6 subscriber, a VPS
|
|
100
|
+
* included, holds at least a /64 and may pick any interface id inside it, so
|
|
101
|
+
* one counter per full address is a fresh counter per request for anyone
|
|
102
|
+
* who rotates: the prefix is what is counted (`2001:db8:1:2:0:0:0:0/64`). An
|
|
103
|
+
* IPv4-mapped IPv6 address (::ffff:192.0.2.1) is its IPv4 address, so the
|
|
104
|
+
* two spellings share a counter. A value that is not an address is counted
|
|
105
|
+
* as it is. ctx.ip itself stays the exact address, for logs.
|
|
106
|
+
*/
|
|
107
|
+
export const rateLimitSubjectOf = (ip, ipv6PrefixLength = DEFAULT_IPV6_RATE_LIMIT_PREFIX) => {
|
|
108
|
+
const groups = parseIpv6(ip);
|
|
109
|
+
if (!groups)
|
|
110
|
+
return ip;
|
|
111
|
+
if (groups.slice(0, 5).every((group) => group === 0) && groups[5] === 0xffff) {
|
|
112
|
+
return `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
|
|
113
|
+
}
|
|
114
|
+
const masked = groups.map((group, index) => {
|
|
115
|
+
const kept = Math.max(0, Math.min(16, ipv6PrefixLength - index * 16));
|
|
116
|
+
return kept === 0 ? 0 : group & ((0xffff << (16 - kept)) & 0xffff);
|
|
117
|
+
});
|
|
118
|
+
return `${masked.map((group) => group.toString(16)).join(":")}/${ipv6PrefixLength}`;
|
|
119
|
+
};
|
|
38
120
|
/**
|
|
39
121
|
* The first trusted header that carries anything, leftmost entry, else the
|
|
40
|
-
* address the gateway observed
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* from anyway.
|
|
122
|
+
* address the gateway observed; a header that always appends the port (see
|
|
123
|
+
* PORT_SUFFIXED_IP_HEADERS) has it taken off. Nothing is trusted by default,
|
|
124
|
+
* and there is no exception for an invoke: the invoke marker is an ordinary
|
|
125
|
+
* request header any HTTP caller can set, so honouring it would let every
|
|
126
|
+
* caller choose its own address. A genuine invoke needs no exception,
|
|
127
|
+
* because the synthesized event carries the end user's address in
|
|
128
|
+
* requestContext.http.sourceIp, which is where `sourceIp` comes from anyway.
|
|
47
129
|
*/
|
|
48
130
|
export const resolveClientIp = (lowercasedHeaders, sourceIp, trustedClientIpHeaders = []) => {
|
|
49
131
|
for (const name of trustedClientIpHeaders) {
|
|
50
|
-
const
|
|
132
|
+
const header = name.toLowerCase();
|
|
133
|
+
const value = lowercasedHeaders[header];
|
|
51
134
|
const first = value ? (value.split(",")[0] ?? "").trim() : "";
|
|
52
135
|
if (first)
|
|
53
|
-
return normalizeClientIp(first);
|
|
136
|
+
return normalizeClientIp(PORT_SUFFIXED_IP_HEADERS.has(header) ? first.replace(/:\d+$/, "") : first);
|
|
54
137
|
}
|
|
55
138
|
return normalizeClientIp(sourceIp) || "";
|
|
56
139
|
};
|
|
@@ -1,48 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Map whose entries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* second.
|
|
2
|
+
* A Map whose entries expire, which every in-memory store here needs:
|
|
3
|
+
* idempotency records, rate-limit counters and session records all key
|
|
4
|
+
* something by a string and all stop mattering at a known second.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* only when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* runs, and an idempotency record for a key that never returns is dead weight
|
|
12
|
-
* for ever. So expiry happens on read AND on an amortized sweep, and no
|
|
13
|
-
* caller has to remember either.
|
|
6
|
+
* Entries expire on read and on an amortized sweep, so no caller has to
|
|
7
|
+
* remember either. Expiring only when the same key is read again would leak
|
|
8
|
+
* in anything long-lived: a counter nobody asks about again, or a record for
|
|
9
|
+
* a key that never returns, would stay for as long as the process runs.
|
|
14
10
|
*
|
|
15
|
-
* Expiry
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* idempotency records, so a flood of short-lived keys could reset a monthly
|
|
25
|
-
* cap. Evicting the soonest-expiring entry costs the caller the least that
|
|
26
|
-
* can be taken, and a flood evicts mostly itself.
|
|
11
|
+
* Expiry bounds how long an entry lives, not how many there are: a workload
|
|
12
|
+
* that never repeats a key (a per-IP counter with a monthly window, an
|
|
13
|
+
* idempotency key per request) accumulates live entries faster than expiry
|
|
14
|
+
* retires them. So the map also holds a ceiling, where it evicts the
|
|
15
|
+
* evictable entries that expire soonest. Insertion order would instead
|
|
16
|
+
* retire the entries with the most life left (long-window counters, day-long
|
|
17
|
+
* idempotency records), letting a flood of short-lived keys reset a monthly
|
|
18
|
+
* cap; soonest-to-expire costs the caller least, and a flood evicts mostly
|
|
19
|
+
* itself.
|
|
27
20
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* chosen, and a caller whose write cannot be made room for is told so
|
|
34
|
-
* (LambderExpiringMapFullError) rather than quietly costing somebody else
|
|
21
|
+
* An idempotency claim lives minutes while the record it settles into lives
|
|
22
|
+
* a day, so at the ceiling it would always be the first victim and two
|
|
23
|
+
* concurrent retries would both execute. An entry written with `evictable:
|
|
24
|
+
* false` is never chosen, and a write that cannot be made room for is
|
|
25
|
+
* refused (LambderExpiringMapFullError) rather than costing somebody else
|
|
35
26
|
* their claim.
|
|
36
27
|
*
|
|
37
|
-
* Times are epoch
|
|
38
|
-
*
|
|
28
|
+
* Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
|
|
29
|
+
* stores and their DynamoDB counterparts say the same thing.
|
|
39
30
|
*/
|
|
40
31
|
/**
|
|
41
32
|
* Thrown by set() when the map is at its ceiling and every entry it holds is
|
|
42
33
|
* protected from eviction. The write did not happen and nothing was dropped
|
|
43
|
-
* to make room for it: the caller decides what to do about a store
|
|
44
|
-
*
|
|
45
|
-
* something this map will trade away.
|
|
34
|
+
* to make room for it: the caller decides what to do about a store full of
|
|
35
|
+
* live claims, and a claim somebody else holds is never traded away.
|
|
46
36
|
*/
|
|
47
37
|
export declare class LambderExpiringMapFullError extends Error {
|
|
48
38
|
constructor(maxEntries: number);
|
|
@@ -97,22 +87,18 @@ export declare class LambderExpiringMap<TValue> {
|
|
|
97
87
|
* Drops a batch of the evictable entries closest to expiring, taking the
|
|
98
88
|
* map from its ceiling down to a floor one batch below it. Reached only
|
|
99
89
|
* when expiry cannot keep up, which means the keys are not repeating.
|
|
100
|
-
* Evicting costs the caller whatever the entry
|
|
101
|
-
* resets, a stored answer re-executes on retry), so the
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* short-lived keys cannot evict a long-window counter.
|
|
90
|
+
* Evicting costs the caller whatever the entry protected (a counter
|
|
91
|
+
* resets, a stored answer re-executes on retry), so the entries taken are
|
|
92
|
+
* those with the least life left, and a flood of short-lived keys cannot
|
|
93
|
+
* evict a long-window counter.
|
|
105
94
|
*
|
|
106
|
-
* A batch rather than
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
|
|
111
|
-
* batches of one percent.
|
|
95
|
+
* A batch rather than one entry, because one leaves the map exactly at
|
|
96
|
+
* its ceiling and the next write pays for another pass: one pass per
|
|
97
|
+
* write for as long as the flood lasts. At 100,000 entries that is 0.56
|
|
98
|
+
* ms per write, against 0.007 ms with batches of one percent.
|
|
112
99
|
*
|
|
113
|
-
* The pass is linear and the sort
|
|
114
|
-
*
|
|
115
|
-
* pathological.
|
|
100
|
+
* The pass is linear and the sort covers only evictable entries, which is
|
|
101
|
+
* affordable because reaching the ceiling at all is already pathological.
|
|
116
102
|
*/
|
|
117
103
|
private evictBatch;
|
|
118
104
|
private sweep;
|
|
@@ -1,65 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Map whose entries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* second.
|
|
2
|
+
* A Map whose entries expire, which every in-memory store here needs:
|
|
3
|
+
* idempotency records, rate-limit counters and session records all key
|
|
4
|
+
* something by a string and all stop mattering at a known second.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* only when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* runs, and an idempotency record for a key that never returns is dead weight
|
|
12
|
-
* for ever. So expiry happens on read AND on an amortized sweep, and no
|
|
13
|
-
* caller has to remember either.
|
|
6
|
+
* Entries expire on read and on an amortized sweep, so no caller has to
|
|
7
|
+
* remember either. Expiring only when the same key is read again would leak
|
|
8
|
+
* in anything long-lived: a counter nobody asks about again, or a record for
|
|
9
|
+
* a key that never returns, would stay for as long as the process runs.
|
|
14
10
|
*
|
|
15
|
-
* Expiry
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* idempotency records, so a flood of short-lived keys could reset a monthly
|
|
25
|
-
* cap. Evicting the soonest-expiring entry costs the caller the least that
|
|
26
|
-
* can be taken, and a flood evicts mostly itself.
|
|
11
|
+
* Expiry bounds how long an entry lives, not how many there are: a workload
|
|
12
|
+
* that never repeats a key (a per-IP counter with a monthly window, an
|
|
13
|
+
* idempotency key per request) accumulates live entries faster than expiry
|
|
14
|
+
* retires them. So the map also holds a ceiling, where it evicts the
|
|
15
|
+
* evictable entries that expire soonest. Insertion order would instead
|
|
16
|
+
* retire the entries with the most life left (long-window counters, day-long
|
|
17
|
+
* idempotency records), letting a flood of short-lived keys reset a monthly
|
|
18
|
+
* cap; soonest-to-expire costs the caller least, and a flood evicts mostly
|
|
19
|
+
* itself.
|
|
27
20
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* chosen, and a caller whose write cannot be made room for is told so
|
|
34
|
-
* (LambderExpiringMapFullError) rather than quietly costing somebody else
|
|
21
|
+
* An idempotency claim lives minutes while the record it settles into lives
|
|
22
|
+
* a day, so at the ceiling it would always be the first victim and two
|
|
23
|
+
* concurrent retries would both execute. An entry written with `evictable:
|
|
24
|
+
* false` is never chosen, and a write that cannot be made room for is
|
|
25
|
+
* refused (LambderExpiringMapFullError) rather than costing somebody else
|
|
35
26
|
* their claim.
|
|
36
27
|
*
|
|
37
|
-
* Times are epoch
|
|
38
|
-
*
|
|
28
|
+
* Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
|
|
29
|
+
* stores and their DynamoDB counterparts say the same thing.
|
|
39
30
|
*/
|
|
40
31
|
import { assertPositiveInteger } from "./LambderOptionChecks.js";
|
|
41
32
|
/** How many writes go by before the map walks itself and drops what has expired. */
|
|
42
33
|
const SWEEP_WRITE_INTERVAL = 256;
|
|
43
34
|
/**
|
|
44
|
-
* Live entries held before
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
35
|
+
* Live entries held before eviction starts. High enough that no ordinary
|
|
36
|
+
* single-process run reaches it, low enough to bound the process: crossing it
|
|
37
|
+
* means a key space that never repeats, where the alternative to evicting is
|
|
38
|
+
* growing until the process dies.
|
|
48
39
|
*/
|
|
49
40
|
const DEFAULT_MAX_ENTRIES = 100_000;
|
|
50
41
|
/**
|
|
51
|
-
* Share of the ceiling one eviction pass reclaims
|
|
52
|
-
*
|
|
53
|
-
* the map pays a full pass per write for as long as the flood lasts; taking a
|
|
54
|
-
* batch amortizes that pass over the writes the headroom absorbs.
|
|
42
|
+
* Share of the ceiling one eviction pass reclaims, so the pass is amortized
|
|
43
|
+
* over the writes the headroom absorbs (see evictBatch).
|
|
55
44
|
*/
|
|
56
45
|
const EVICTION_BATCH_SHARE = 0.01;
|
|
57
46
|
/**
|
|
58
47
|
* Thrown by set() when the map is at its ceiling and every entry it holds is
|
|
59
48
|
* protected from eviction. The write did not happen and nothing was dropped
|
|
60
|
-
* to make room for it: the caller decides what to do about a store
|
|
61
|
-
*
|
|
62
|
-
* something this map will trade away.
|
|
49
|
+
* to make room for it: the caller decides what to do about a store full of
|
|
50
|
+
* live claims, and a claim somebody else holds is never traded away.
|
|
63
51
|
*/
|
|
64
52
|
export class LambderExpiringMapFullError extends Error {
|
|
65
53
|
constructor(maxEntries) {
|
|
@@ -160,22 +148,18 @@ export class LambderExpiringMap {
|
|
|
160
148
|
* Drops a batch of the evictable entries closest to expiring, taking the
|
|
161
149
|
* map from its ceiling down to a floor one batch below it. Reached only
|
|
162
150
|
* when expiry cannot keep up, which means the keys are not repeating.
|
|
163
|
-
* Evicting costs the caller whatever the entry
|
|
164
|
-
* resets, a stored answer re-executes on retry), so the
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* short-lived keys cannot evict a long-window counter.
|
|
151
|
+
* Evicting costs the caller whatever the entry protected (a counter
|
|
152
|
+
* resets, a stored answer re-executes on retry), so the entries taken are
|
|
153
|
+
* those with the least life left, and a flood of short-lived keys cannot
|
|
154
|
+
* evict a long-window counter.
|
|
168
155
|
*
|
|
169
|
-
* A batch rather than
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
|
|
174
|
-
* batches of one percent.
|
|
156
|
+
* A batch rather than one entry, because one leaves the map exactly at
|
|
157
|
+
* its ceiling and the next write pays for another pass: one pass per
|
|
158
|
+
* write for as long as the flood lasts. At 100,000 entries that is 0.56
|
|
159
|
+
* ms per write, against 0.007 ms with batches of one percent.
|
|
175
160
|
*
|
|
176
|
-
* The pass is linear and the sort
|
|
177
|
-
*
|
|
178
|
-
* pathological.
|
|
161
|
+
* The pass is linear and the sort covers only evictable entries, which is
|
|
162
|
+
* affordable because reaching the ceiling at all is already pathological.
|
|
179
163
|
*/
|
|
180
164
|
evictBatch(justWritten) {
|
|
181
165
|
const floorSize = Math.max(0, this.maxEntries - this.evictionBatchSize);
|
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* A failed import is only half of "no such thing". The other half is a
|
|
10
10
|
* bundler: package.json maps fs, path, zlib and crypto to `false` for the
|
|
11
|
-
* browser, and webpack, Vite and esbuild
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* module exports; a module that cannot answer it is not the module.
|
|
11
|
+
* browser, and webpack, Vite and esbuild resolve such an import to a stub
|
|
12
|
+
* object rather than rejecting it, so a truthiness test would call the stub
|
|
13
|
+
* usable and the caller would die on its first real call. `expect` names a
|
|
14
|
+
* function the genuine module exports; a module without it is not the module.
|
|
16
15
|
*
|
|
17
|
-
* The answer
|
|
18
|
-
*
|
|
16
|
+
* The answer, absence included, is memoized, so the probe runs once per
|
|
17
|
+
* module.
|
|
19
18
|
*/
|
|
20
19
|
const loadNodeModule = (load, expect) => {
|
|
21
20
|
let pending = null;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks every option that names a count, a size or a duration goes
|
|
3
|
-
* through at creation, so a bad value
|
|
4
|
-
* everywhere
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* through at creation, so a bad value meets one predicate and one wording
|
|
4
|
+
* everywhere. `name` is the option as the reader wrote it
|
|
5
|
+
* (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
|
|
6
|
+
* fix.
|
|
7
7
|
*/
|
|
8
8
|
/** A safe integer of one or more; returns it so the check reads as an assignment. */
|
|
9
9
|
export declare const assertPositiveInteger: (value: unknown, name: string) => number;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks every option that names a count, a size or a duration goes
|
|
3
|
-
* through at creation, so a bad value
|
|
4
|
-
* everywhere
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* through at creation, so a bad value meets one predicate and one wording
|
|
4
|
+
* everywhere. `name` is the option as the reader wrote it
|
|
5
|
+
* (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
|
|
6
|
+
* fix.
|
|
7
7
|
*/
|
|
8
8
|
const describe = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
9
9
|
/** A safe integer of one or more; returns it so the check reads as an assignment. */
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Marks an object as a response without anyone having to import the class to
|
|
3
3
|
* ask. Layers that must recognise one but must not depend on core at runtime
|
|
4
4
|
* (the guards engine, which runs in the browser too) test for this key.
|
|
5
|
-
* Symbol.for keeps it true across realms and
|
|
6
|
-
* package. Defined
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Symbol.for keeps it true across realms and duplicate copies of the
|
|
6
|
+
* package. Defined in shared so the class that carries the brand and the
|
|
7
|
+
* engine that checks it import one constant: a hand-typed copy of the
|
|
8
|
+
* symbol's name would keep compiling after a rename while the runtime check
|
|
9
|
+
* silently stopped matching.
|
|
10
10
|
*/
|
|
11
11
|
export declare const LAMBDER_RESPONSE_BRAND: unique symbol;
|
|
12
12
|
/**
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Marks an object as a response without anyone having to import the class to
|
|
3
3
|
* ask. Layers that must recognise one but must not depend on core at runtime
|
|
4
4
|
* (the guards engine, which runs in the browser too) test for this key.
|
|
5
|
-
* Symbol.for keeps it true across realms and
|
|
6
|
-
* package. Defined
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Symbol.for keeps it true across realms and duplicate copies of the
|
|
6
|
+
* package. Defined in shared so the class that carries the brand and the
|
|
7
|
+
* engine that checks it import one constant: a hand-typed copy of the
|
|
8
|
+
* symbol's name would keep compiling after a rename while the runtime check
|
|
9
|
+
* silently stopped matching.
|
|
10
10
|
*/
|
|
11
11
|
export const LAMBDER_RESPONSE_BRAND = Symbol.for("lambder.response");
|
|
12
12
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The small type utilities more than one module needs.
|
|
3
3
|
*
|
|
4
|
-
* Nothing here is Lambder's own vocabulary: these are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
|
|
5
|
+
* not ship, written once so they cannot drift in name or meaning across the
|
|
6
|
+
* modules that use them.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* A value a caller may hand back either synchronously or as a promise. Every
|
|
@@ -18,15 +18,14 @@ export type MaybePromise<T> = T | Promise<T>;
|
|
|
18
18
|
*
|
|
19
19
|
* An all-optional map is inhabited by `{}`, which would let `guards: {}`
|
|
20
20
|
* satisfy requireSessionApiGuards / requirePublicApiGuards at the type level
|
|
21
|
-
* while declaring no guard
|
|
21
|
+
* while declaring no guard: the option is present, so the required-field
|
|
22
22
|
* check passes, and it normalizes to zero entries, so nothing runs. Requiring
|
|
23
23
|
* the chosen key also rejects `{ theGuard: undefined }`, which an optional
|
|
24
|
-
* property accepts and which would
|
|
24
|
+
* property accepts and which would reach the guard's handler with an
|
|
25
25
|
* undefined param.
|
|
26
26
|
*
|
|
27
|
-
* Option-neutral
|
|
28
|
-
*
|
|
29
|
-
* did not.
|
|
27
|
+
* Option-neutral: the rate-limit option's map form uses it too, so
|
|
28
|
+
* `rateLimit: {}` and `guards: {}` are refused alike.
|
|
30
29
|
*/
|
|
31
30
|
export type LambderNonEmptyOptionMap<TMap> = {
|
|
32
31
|
[K in keyof TMap]-?: Required<Pick<TMap, K>> & Omit<TMap, K>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The small type utilities more than one module needs.
|
|
3
3
|
*
|
|
4
|
-
* Nothing here is Lambder's own vocabulary: these are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
|
|
5
|
+
* not ship, written once so they cannot drift in name or meaning across the
|
|
6
|
+
* modules that use them.
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounding the caller-supplied field of a tracker or scope key.
|
|
3
|
+
*
|
|
4
|
+
* The rate-limit engine and the idempotency engine each build a store key
|
|
5
|
+
* around a field only the caller controls: a custom rate-limit key or a
|
|
6
|
+
* session key, a callerIdentity or a session key. A store has a key limit of
|
|
7
|
+
* its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
|
|
8
|
+
* past it by throwing, and both engines fail open on a store throw by
|
|
9
|
+
* default: a long enough field (a 3,000-character email, a device token)
|
|
10
|
+
* would turn the rate limit or the idempotency off for that caller, in
|
|
11
|
+
* silence, while the table held the field in plain text. So the field is
|
|
12
|
+
* bounded before any store sees it, in one implementation the two engines
|
|
13
|
+
* share, so they cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
|
|
17
|
+
* hex>` once it does not. The digest keeps distinct callers on distinct
|
|
18
|
+
* counters and scopes, and a value that fits stays readable in the table.
|
|
19
|
+
*/
|
|
20
|
+
export declare const boundKeyField: (kind: string, value: string) => Promise<string>;
|