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.
Files changed (207) hide show
  1. package/CHANGELOG.md +933 -3
  2. package/README.md +41 -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 +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -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 +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -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 +140 -75
  41. package/dist/core/Lambder.js +347 -227
  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 +21 -7
  51. package/dist/core/LambderFiles.js +62 -34
  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 +29 -9
  73. package/dist/invoke/LambderLambdaEvent.js +40 -22
  74. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  75. package/dist/invoke/lambderHandlerTransport.js +15 -18
  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 +1 -1
  93. package/dist/mock.js +2 -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 +124 -46
  99. package/dist/session/LambderSessionManager.js +262 -137
  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/LambderTypeUtilities.d.ts +7 -8
  134. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  135. package/dist/shared/util/boundKeyField.d.ts +20 -0
  136. package/dist/shared/util/boundKeyField.js +34 -0
  137. package/dist/shared/util/canonicalJson.d.ts +11 -0
  138. package/dist/shared/util/canonicalJson.js +28 -0
  139. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  140. package/dist/shared/util/joinKeyFields.js +22 -0
  141. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  142. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  143. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  144. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  145. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  146. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  147. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  148. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  149. package/dist/shared/wire/LambderApiSignature.js +16 -19
  150. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  151. package/dist/shared/wire/LambderCallOptions.js +9 -11
  152. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  153. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  154. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  155. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  156. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  157. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  158. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  159. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  160. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  161. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  162. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  163. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  164. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  165. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  166. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  167. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  168. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  169. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  170. package/dist/stores/LambderCacheFiller.js +119 -0
  171. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  172. package/dist/stores/LambderCacheKeys.js +54 -0
  173. package/dist/stores/LambderCacheValues.d.ts +45 -0
  174. package/dist/stores/LambderCacheValues.js +74 -0
  175. package/dist/stores/LambderDdbCache.d.ts +121 -56
  176. package/dist/stores/LambderDdbCache.js +528 -225
  177. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  178. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  179. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  180. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  181. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  182. package/dist/stores/LambderDdbSdk.js +79 -33
  183. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  184. package/dist/stores/LambderDdbSessionStore.js +119 -47
  185. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  186. package/dist/stores/LambderHttpFileSource.js +15 -13
  187. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  188. package/dist/stores/LambderMemoryCache.js +113 -0
  189. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  190. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  191. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  192. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  193. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  194. package/dist/stores/LambderMemorySessionStore.js +38 -19
  195. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  196. package/dist/stores/LambderS3FileSource.js +12 -7
  197. package/dist/testing/LambderTestApp.d.ts +21 -23
  198. package/dist/testing/LambderTestApp.js +22 -24
  199. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  200. package/dist/testing/LambderTestVisitor.js +15 -15
  201. package/dist/testing.d.ts +1 -0
  202. package/dist/testing.js +1 -0
  203. package/package.json +12 -3
  204. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  205. package/dist/api/LambderApiPolicyEngine.js +0 -85
  206. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  207. 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, and IPv6 has many spellings for
17
- * one address; each variant would otherwise be its own counter, which is a
18
- * limit that does not limit. Anything longer than the longest valid address
19
- * is truncated rather than trusted as a key.
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. Nothing is trusted by default, and there is
25
- * no exception for an invoke: the marker header that would have signalled
26
- * one is an ordinary request header that any HTTP caller can set, so
27
- * honouring it would hand every caller the value again. A genuine invoke
28
- * needs no exception, because the synthesized event carries the end user's
29
- * address in requestContext.http.sourceIp, which is where `sourceIp` comes
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, and IPv6 has many spellings for
19
- * one address; each variant would otherwise be its own counter, which is a
20
- * limit that does not limit. Anything longer than the longest valid address
21
- * is truncated rather than trusted as a key.
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. Nothing is trusted by default, and there is
41
- * no exception for an invoke: the marker header that would have signalled
42
- * one is an ordinary request header that any HTTP caller can set, so
43
- * honouring it would hand every caller the value again. A genuine invoke
44
- * needs no exception, because the synthesized event carries the end user's
45
- * address in requestContext.http.sourceIp, which is where `sourceIp` comes
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 value = lowercasedHeaders[name.toLowerCase()];
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 have an expiry, which is the one thing every in-memory
3
- * store here needs: idempotency records, rate-limit counters and session
4
- * records all key something by a string and all stop mattering at a known
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
- * Written once because a store that hand-rolls it tends to expire an entry
8
- * only when something asks for that exact key again, which
9
- * is fine for a test and a slow leak in anything long-lived: a rate-limit
10
- * counter nobody asks about again is dead weight for as long as the process
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 alone bounds how long an entry lives, not how many there are: a
16
- * workload that never repeats a key (a rate-limit counter per IP with a
17
- * monthly window, an idempotency key per request) accumulates live entries
18
- * faster than any expiry retires them. So the map also holds a ceiling and
19
- * evicts once it is reached, which is what makes "in memory" a bounded claim
20
- * rather than a slower leak. It evicts whatever expires SOONEST among the
21
- * entries that may be evicted at all, never whatever was written earliest:
22
- * insertion order would retire exactly the entries with the most life left in
23
- * them, which are the long-window rate-limit counters and the day-long
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
- * "Soonest to expire" is the wrong answer for one kind of entry, though, and
29
- * it is the one where the cost is highest: an idempotency claim lives for a
30
- * few minutes while the settled record it becomes lives for a day, so at the
31
- * ceiling the claim was always the first victim and two concurrent retries
32
- * both executed. An entry written with `evictable: false` is therefore never
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 SECONDS, matching the TTL attribute DynamoDB uses, so the
38
- * memory stores and their DynamoDB counterparts say the same thing.
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 that is
44
- * full of live claims, and the claim already held by somebody else is not
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 was protecting (a counter
101
- * resets, a stored answer re-executes on retry), so the ones taken are
102
- * always the ones with the least life left: the soonest to expire were
103
- * going to be lost first anyway, and choosing them means a flood of
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 a single entry because a single one leaves the map
107
- * exactly at its ceiling, so the next write crosses it again and pays for
108
- * another pass: one pass per write, for as long as the flood lasts. The
109
- * headroom this leaves is what the following writes spend. Measured at
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 is over the evictable entries only,
114
- * which is affordable because reaching the ceiling at all is already
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 have an expiry, which is the one thing every in-memory
3
- * store here needs: idempotency records, rate-limit counters and session
4
- * records all key something by a string and all stop mattering at a known
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
- * Written once because a store that hand-rolls it tends to expire an entry
8
- * only when something asks for that exact key again, which
9
- * is fine for a test and a slow leak in anything long-lived: a rate-limit
10
- * counter nobody asks about again is dead weight for as long as the process
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 alone bounds how long an entry lives, not how many there are: a
16
- * workload that never repeats a key (a rate-limit counter per IP with a
17
- * monthly window, an idempotency key per request) accumulates live entries
18
- * faster than any expiry retires them. So the map also holds a ceiling and
19
- * evicts once it is reached, which is what makes "in memory" a bounded claim
20
- * rather than a slower leak. It evicts whatever expires SOONEST among the
21
- * entries that may be evicted at all, never whatever was written earliest:
22
- * insertion order would retire exactly the entries with the most life left in
23
- * them, which are the long-window rate-limit counters and the day-long
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
- * "Soonest to expire" is the wrong answer for one kind of entry, though, and
29
- * it is the one where the cost is highest: an idempotency claim lives for a
30
- * few minutes while the settled record it becomes lives for a day, so at the
31
- * ceiling the claim was always the first victim and two concurrent retries
32
- * both executed. An entry written with `evictable: false` is therefore never
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 SECONDS, matching the TTL attribute DynamoDB uses, so the
38
- * memory stores and their DynamoDB counterparts say the same thing.
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 the oldest writes start being evicted. High enough
45
- * that no ordinary single-process run reaches it, low enough to bound the
46
- * process: crossing it means a key space that never repeats, where the
47
- * alternative to evicting is growing until the process dies.
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. Evicting exactly the one
52
- * entry that crossed the ceiling means every later write crosses it again, so
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 that is
61
- * full of live claims, and the claim already held by somebody else is not
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 was protecting (a counter
164
- * resets, a stored answer re-executes on retry), so the ones taken are
165
- * always the ones with the least life left: the soonest to expire were
166
- * going to be lost first anyway, and choosing them means a flood of
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 a single entry because a single one leaves the map
170
- * exactly at its ceiling, so the next write crosses it again and pays for
171
- * another pass: one pass per write, for as long as the flood lasts. The
172
- * headroom this leaves is what the following writes spend. Measured at
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 is over the evictable entries only,
177
- * which is affordable because reaching the ceiling at all is already
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 each honour that by resolving the
12
- * import to a stub module rather than by rejecting it. Those stubs are
13
- * objects, so a truthiness test calls them usable and the caller dies on the
14
- * first real function it reaches. `expect` names a function the genuine
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 is memoized either way, absence included, so the probe runs once
18
- * per module however often a request asks for it.
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 is one wording and one predicate
4
- * everywhere rather than seven spellings of the same rule. `name` is the
5
- * option as the reader wrote it (`maxResponseBytes`, `session.ttlSeconds`),
6
- * so the error says which one to fix.
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 is one wording and one predicate
4
- * everywhere rather than seven spellings of the same rule. `name` is the
5
- * option as the reader wrote it (`maxResponseBytes`, `session.ttlSeconds`),
6
- * so the error says which one to fix.
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 across duplicate copies of the
6
- * package. Defined here, in shared, so the class that carries the brand and
7
- * the engine that checks for it import the one constant: a hand-typed copy
8
- * of the symbol's name would keep compiling after a rename while the runtime
9
- * check silently stopped matching.
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 across duplicate copies of the
6
- * package. Defined here, in shared, so the class that carries the brand and
7
- * the engine that checks for it import the one constant: a hand-typed copy
8
- * of the symbol's name would keep compiling after a rename while the runtime
9
- * check silently stopped matching.
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 the shapes TypeScript
5
- * does not ship, written once because the alternative is the same three
6
- * lines in every module that wants them, drifting in name and in meaning.
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 at all: the option is present, so the required-field
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 otherwise reach the guard's handler with an
24
+ * property accepts and which would reach the guard's handler with an
25
25
  * undefined param.
26
26
  *
27
- * Option-neutral, and the rate-limit option's map form is built with the same
28
- * type: the two had drifted, and `rateLimit: {}` compiled while `guards: {}`
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 the shapes TypeScript
5
- * does not ship, written once because the alternative is the same three
6
- * lines in every module that wants them, drifting in name and in meaning.
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>;