lambder 7.3.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/CHANGELOG.md +1047 -3
  2. package/README.md +46 -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/ContractTypePrinter.d.ts +85 -0
  27. package/dist/build/ContractTypePrinter.js +402 -0
  28. package/dist/build/freshProcessVerifier.d.ts +13 -0
  29. package/dist/build/freshProcessVerifier.js +19 -0
  30. package/dist/build/moduleLocation.d.ts +11 -0
  31. package/dist/build/moduleLocation.js +6 -0
  32. package/dist/build/writeApiContract.d.ts +78 -0
  33. package/dist/build/writeApiContract.js +302 -0
  34. package/dist/build/writeApiSignatures.d.ts +114 -0
  35. package/dist/build/writeApiSignatures.js +217 -0
  36. package/dist/build/writeFileAtomically.d.ts +8 -0
  37. package/dist/build/writeFileAtomically.js +22 -0
  38. package/dist/build.d.ts +14 -0
  39. package/dist/build.js +11 -0
  40. package/dist/client/LambderCaller.d.ts +13 -44
  41. package/dist/client/LambderCaller.js +77 -84
  42. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  43. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  44. package/dist/client/LambderUploadRunner.d.ts +96 -0
  45. package/dist/client/LambderUploadRunner.js +234 -0
  46. package/dist/client/lambderFetchTransport.d.ts +4 -1
  47. package/dist/client/lambderFetchTransport.js +52 -28
  48. package/dist/client.d.ts +9 -3
  49. package/dist/client.js +6 -1
  50. package/dist/core/Lambder.d.ts +143 -79
  51. package/dist/core/Lambder.js +350 -231
  52. package/dist/core/LambderContext.d.ts +82 -15
  53. package/dist/core/LambderContext.js +107 -20
  54. package/dist/core/LambderCors.d.ts +21 -3
  55. package/dist/core/LambderCors.js +35 -16
  56. package/dist/core/LambderCrashHandling.d.ts +40 -0
  57. package/dist/core/LambderCrashHandling.js +97 -0
  58. package/dist/core/LambderCreateOptions.d.ts +151 -75
  59. package/dist/core/LambderCreateOptions.js +16 -23
  60. package/dist/core/LambderFiles.d.ts +21 -7
  61. package/dist/core/LambderFiles.js +62 -34
  62. package/dist/core/LambderIndexHtml.js +12 -11
  63. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  64. package/dist/core/LambderPolicyBuilders.js +17 -5
  65. package/dist/core/LambderPublicFiles.d.ts +11 -5
  66. package/dist/core/LambderPublicFiles.js +32 -4
  67. package/dist/core/LambderRequestPath.d.ts +43 -0
  68. package/dist/core/LambderRequestPath.js +63 -0
  69. package/dist/core/LambderResponse.d.ts +26 -5
  70. package/dist/core/LambderResponse.js +157 -70
  71. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  72. package/dist/core/LambderResponseBuilder.js +64 -3
  73. package/dist/core/LambderRouting.d.ts +2 -3
  74. package/dist/core/LambderRouting.js +22 -7
  75. package/dist/core/LambderTemplatingEngine.js +211 -32
  76. package/dist/index.d.ts +25 -8
  77. package/dist/index.js +13 -4
  78. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  79. package/dist/invoke/LambderInvokeCaller.js +76 -66
  80. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  81. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  82. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  83. package/dist/invoke/LambderLambdaEvent.js +40 -22
  84. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  85. package/dist/invoke/lambderHandlerTransport.js +15 -18
  86. package/dist/mock/LambderMockApp.d.ts +67 -83
  87. package/dist/mock/LambderMockApp.js +167 -153
  88. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  89. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  90. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  91. package/dist/mock/LambderMockCallRecorder.js +19 -28
  92. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  93. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  94. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  95. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  96. package/dist/mock/LambderMockFailureInjector.js +3 -6
  97. package/dist/mock/LambderMockTypes.d.ts +78 -108
  98. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  99. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  100. package/dist/mock/lambderMockMswHandler.d.ts +43 -33
  101. package/dist/mock/lambderMockMswHandler.js +50 -39
  102. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  103. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  104. package/dist/mock.d.ts +4 -1
  105. package/dist/mock.js +6 -3
  106. package/dist/session/LambderSessionController.d.ts +108 -89
  107. package/dist/session/LambderSessionController.js +187 -168
  108. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  109. package/dist/session/LambderSessionCrypto.js +26 -12
  110. package/dist/session/LambderSessionManager.d.ts +124 -46
  111. package/dist/session/LambderSessionManager.js +262 -137
  112. package/dist/shared/LambderHtml.d.ts +42 -3
  113. package/dist/shared/LambderHtml.js +127 -7
  114. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  115. package/dist/shared/LambderHtmlPositions.js +652 -0
  116. package/dist/shared/LambderI18n.d.ts +10 -11
  117. package/dist/shared/LambderI18n.js +33 -21
  118. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  119. package/dist/shared/contracts/LambderCache.js +11 -0
  120. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  121. package/dist/shared/contracts/LambderFileSource.js +5 -8
  122. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  123. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  124. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  125. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  126. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  127. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  128. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  129. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  130. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  131. package/dist/shared/transport/LambderApiTransport.js +7 -7
  132. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  133. package/dist/shared/transport/LambderCookieJar.js +54 -66
  134. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  135. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  136. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  137. package/dist/shared/util/LambderCallAbort.js +5 -5
  138. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  139. package/dist/shared/util/LambderClientIp.js +96 -13
  140. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  141. package/dist/shared/util/LambderContentDisposition.js +13 -0
  142. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  143. package/dist/shared/util/LambderExpiringMap.js +41 -57
  144. package/dist/shared/util/LambderNodeModules.js +6 -7
  145. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  146. package/dist/shared/util/LambderOptionChecks.js +4 -4
  147. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  148. package/dist/shared/util/LambderResponseBrand.js +5 -5
  149. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  150. package/dist/shared/util/LambderTextDigest.js +11 -5
  151. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  152. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  153. package/dist/shared/util/boundKeyField.d.ts +20 -0
  154. package/dist/shared/util/boundKeyField.js +34 -0
  155. package/dist/shared/util/canonicalJson.d.ts +11 -0
  156. package/dist/shared/util/canonicalJson.js +28 -0
  157. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  158. package/dist/shared/util/joinKeyFields.js +22 -0
  159. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  160. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  161. package/dist/shared/wire/LambderApiContract.d.ts +98 -53
  162. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  163. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  164. package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
  165. package/dist/shared/wire/LambderApiRefusal.js +42 -7
  166. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  167. package/dist/shared/wire/LambderApiSignature.js +16 -19
  168. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  169. package/dist/shared/wire/LambderCallOptions.js +9 -11
  170. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  171. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  172. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  173. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  174. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  175. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  176. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  177. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  178. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  179. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  180. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  181. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  182. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  183. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  184. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  185. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  186. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  187. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  188. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  189. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  190. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  191. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  192. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  193. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  194. package/dist/stores/LambderCacheFiller.js +119 -0
  195. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  196. package/dist/stores/LambderCacheKeys.js +54 -0
  197. package/dist/stores/LambderCacheValues.d.ts +45 -0
  198. package/dist/stores/LambderCacheValues.js +74 -0
  199. package/dist/stores/LambderDdbCache.d.ts +121 -56
  200. package/dist/stores/LambderDdbCache.js +528 -225
  201. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  202. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  203. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  204. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  205. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  206. package/dist/stores/LambderDdbSdk.js +80 -38
  207. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  208. package/dist/stores/LambderDdbSessionStore.js +119 -47
  209. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  210. package/dist/stores/LambderHttpFileSource.js +15 -13
  211. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  212. package/dist/stores/LambderMemoryCache.js +113 -0
  213. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  214. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  215. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  216. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  217. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  218. package/dist/stores/LambderMemorySessionStore.js +38 -19
  219. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  220. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  221. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  222. package/dist/stores/LambderS3FileSource.js +12 -7
  223. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  224. package/dist/stores/LambderS3UploadBucket.js +144 -0
  225. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  226. package/dist/stores/LambderSdkInstallHint.js +14 -0
  227. package/dist/testing/LambderTestApp.d.ts +23 -25
  228. package/dist/testing/LambderTestApp.js +22 -24
  229. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  230. package/dist/testing/LambderTestVisitor.js +15 -15
  231. package/dist/testing.d.ts +3 -0
  232. package/dist/testing.js +2 -0
  233. package/package.json +26 -3
  234. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  235. package/dist/api/LambderApiPolicyEngine.js +0 -85
  236. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  237. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -6,10 +6,9 @@
6
6
  import { Cookie, CookieJar as ToughCookieJar, defaultPath, pathMatch } from "tough-cookie";
7
7
  /**
8
8
  * Stands in for the host of a jar that was never told one. Every store and
9
- * every read of such a jar uses it, so the jar is self-consistent: it behaves
10
- * as the browser of one unnamed host. `.invalid` is reserved by the IANA and
11
- * can never be a real name, so a cookie parked here can never match a real
12
- * target by accident.
9
+ * read of such a jar uses it, so the jar behaves consistently as the browser
10
+ * of one unnamed host. `.invalid` is IANA-reserved, so a cookie parked here
11
+ * can never match a real target by accident.
13
12
  */
14
13
  const UNNAMED_JAR_HOST = "lambder-cookie-jar.invalid";
15
14
  /** The host without its port, lowercased: "App.Test:3000" is "app.test", "[::1]:8080" is "[::1]". */
@@ -28,12 +27,11 @@ const urlFor = (host, path, secure) => `${secure ? "https" : "http"}://${host}${
28
27
  /**
29
28
  * When a cookie actually dies, as an absolute moment.
30
29
  *
31
- * Max-Age and Expires are stored separately and Max-Age wins, so reading the
32
- * `expires` field alone calls a `Max-Age=0` deletion immortal. The library's
33
- * own expiryTime() resolves Max-Age against whatever moment it is handed, and
34
- * against lastAccessed when handed nothing, so neither answers "when does this
35
- * die" on a fixed clock. A browser measures Max-Age from when the cookie
36
- * arrived, which is its creation.
30
+ * Max-Age and Expires are stored separately and Max-Age wins, so reading
31
+ * `expires` alone would call a `Max-Age=0` deletion immortal. The library's
32
+ * expiryTime() resolves Max-Age against whatever moment it is handed (or
33
+ * lastAccessed), so it cannot answer "when does this die" on a fixed clock.
34
+ * A browser measures Max-Age from when the cookie arrived, its creation.
37
35
  */
38
36
  const cookieExpiryAt = (cookie, now) => {
39
37
  if (typeof cookie.maxAge === "number") {
@@ -50,8 +48,8 @@ const storedFromCookie = (cookie, now) => {
50
48
  name: cookie.key ?? "",
51
49
  value: cookie.value ?? "",
52
50
  domain: hostOnly ? undefined : domain,
53
- // A jar that never learned a host parked this under the stand-in,
54
- // which is an implementation detail rather than something it knows.
51
+ // A jar that never learned a host parks cookies under the stand-in,
52
+ // an implementation detail rather than something it knows.
55
53
  ...(hostOnly && domain !== undefined && domain !== UNNAMED_JAR_HOST ? { host: domain } : {}),
56
54
  path: typeof cookie.path === "string" ? cookie.path : "/",
57
55
  expires: expiryAt,
@@ -68,11 +66,11 @@ export const parseSetCookie = (header, now, requestPath) => {
68
66
  const parsed = Cookie.parse(header, { loose: false });
69
67
  if (!parsed)
70
68
  return null;
71
- // Cookie.parse stamps creation with the real clock, but the caller's `now`
72
- // is the moment this header arrived, and Max-Age is measured from there.
69
+ // Cookie.parse stamps creation with the real clock, but Max-Age is
70
+ // measured from the caller's `now`, when this header arrived.
73
71
  parsed.creation = new Date(now);
74
- // Resolve Max-Age against Expires on the caller's clock, the way the jar
75
- // itself will, so a test moving time forward reads the same expiry here.
72
+ // Max-Age against Expires on the caller's clock, as the jar resolves it,
73
+ // so a test moving time forward reads the same expiry here.
76
74
  return {
77
75
  ...storedFromCookie(parsed, now),
78
76
  // Cookie.parse leaves an absent Path absent; the default-path is the
@@ -85,28 +83,23 @@ export const parseSetCookie = (header, now, requestPath) => {
85
83
  * in-process handler transport in a Node test, and the mock runtime's direct
86
84
  * transport. It stores what an answer's Set-Cookie headers set, honours their
87
85
  * expiry and deletion, and hands back the Cookie pairs the next request should
88
- * carry. One jar is one browser; two jars are two.
86
+ * carry. One jar is one browser.
89
87
  *
90
- * The rules themselves are tough-cookie's, which is the reference
91
- * implementation of RFC 6265 and carries the public suffix list: domain and
92
- * path matching, default-path, Max-Age against Expires, Secure, HttpOnly, and
93
- * the __Host-/__Secure- prefixes. That list is the part worth importing rather
94
- * than writing. A hand-rolled check can tell that `Domain=com` is a registry
95
- * suffix by counting labels, and cannot tell that `co.uk` is one, so a
96
- * hand-rolled jar either trusts `Domain=co.uk` or bans every two-label domain.
88
+ * The rules (domain and path matching, default-path, Max-Age against Expires,
89
+ * Secure, HttpOnly, the __Host-/__Secure- prefixes) are tough-cookie's, the
90
+ * reference RFC 6265 implementation, imported for its public suffix list:
91
+ * counting labels can tell `Domain=com` is a registry suffix but not `co.uk`,
92
+ * so a hand-rolled jar either trusts `Domain=co.uk` or bans every two-label
93
+ * domain.
97
94
  *
98
- * What stays Lambder's is the shape of the questions a transport asks: whole
99
- * Set-Cookie header lists in (storeSetCookies), `name=value` pairs out
100
- * (cookiePairs), and a target given as a host and path rather than a URL,
101
- * since a transport that never speaks HTTP has no URL to give. A field the
102
- * caller omits is one it could not know, and an unknown field matches
103
- * anything: a jar pointed at a single host is the ordinary case, and refusing
104
- * to answer it until it can name that host would make the common setup the
105
- * awkward one.
95
+ * What stays Lambder's is the shape of a transport's questions: Set-Cookie
96
+ * header lists in (storeSetCookies), `name=value` pairs out (cookiePairs),
97
+ * and a target given as host and path, since a transport that never speaks
98
+ * HTTP has no URL. An omitted field matches anything, because a jar pointed
99
+ * at a single host is the ordinary case and should not have to name it.
106
100
  *
107
- * SameSite is stored but never consulted. It answers "did another site
108
- * initiate this", and a transport call has no initiating site: every call here
109
- * is same-site by construction.
101
+ * SameSite is stored but never consulted: it answers "did another site
102
+ * initiate this", and every transport call is same-site by construction.
110
103
  */
111
104
  export class LambderCookieJar {
112
105
  // prefixSecurity "silent" drops a __Host-/__Secure- cookie that breaks its
@@ -129,25 +122,23 @@ export class LambderCookieJar {
129
122
  * came from. Its `host` is the sending host, which every Domain is checked
130
123
  * against, and its `path` is the default Path of a cookie that names none.
131
124
  *
132
- * A Domain the sender is not under does not narrow a cookie, it voids it
133
- * (RFC 6265 section 5.3 step 6), and so does a Domain that is a public
134
- * suffix. Both are how evil.example.com would otherwise plant a cookie
135
- * that bank.example.com is handed on the next call.
125
+ * A Domain the sender is not under voids the cookie rather than narrowing
126
+ * it (RFC 6265 section 5.3 step 6), and so does a public-suffix Domain.
127
+ * Otherwise evil.example.com could plant a cookie that bank.example.com
128
+ * is handed on the next call.
136
129
  */
137
130
  storeSetCookies(headers, request = {}) {
138
- // The whole target, `secure` included: a cookie is judged against the
139
- // channel it actually arrived on. Hardcoding https here accepted
140
- // Secure cookies from a plain-http answer and then never sent one, so
141
- // the jar held a session it could not use and said nothing.
131
+ // A cookie is judged against the channel it arrived on. Assuming https
132
+ // would accept Secure cookies from a plain-http answer and then never
133
+ // send them, leaving the jar silently holding an unusable session.
142
134
  const secure = request.secure !== false;
143
135
  const url = urlFor(this.hostFor(request.host), request.path, secure);
144
136
  const now = new Date(this.now());
145
137
  for (const header of headers) {
146
- // tough-cookie checks the Domain, the prefixes and HttpOnly
147
- // against the URL, but leaves the Secure attribute to the caller:
148
- // RFC 6265 lets plain http set one and browsers stopped allowing
149
- // it. A cookie this jar would refuse to send is one it refuses to
150
- // keep.
138
+ // tough-cookie checks Domain, prefixes and HttpOnly against the
139
+ // URL but leaves Secure to the caller: RFC 6265 lets plain http
140
+ // set one, browsers do not. A cookie this jar would refuse to send
141
+ // is one it refuses to keep.
151
142
  if (!secure && Cookie.parse(header, { loose: false })?.secure)
152
143
  continue;
153
144
  // ignoreError: a cookie a browser would refuse is one this jar
@@ -166,15 +157,14 @@ export class LambderCookieJar {
166
157
  }
167
158
  /**
168
159
  * The Cookie header pairs the next request carries, as `name=value`, in
169
- * the order RFC 6265 section 5.4 puts them in: the longest Path first,
170
- * and among equal paths the one set first. Servers that read only the
171
- * first value of a repeated name depend on that order, and so does any
172
- * test reasoning about which of two same-named cookies wins.
160
+ * RFC 6265 section 5.4 order: longest Path first, and among equal paths
161
+ * the one set first. Servers that read only the first value of a repeated
162
+ * name depend on that order, as does any test about which of two
163
+ * same-named cookies wins.
173
164
  *
174
- * Only the cookies whose scope covers the target travel. A field the
175
- * target leaves out is one the caller could not know, and matches
176
- * anything: a caller that cannot name its own host still gets the cookies
177
- * of the one host its jar talks to.
165
+ * Only cookies whose scope covers the target travel. An omitted target
166
+ * field matches anything, so a caller that cannot name its host still
167
+ * gets the cookies of the one host its jar talks to.
178
168
  */
179
169
  cookiePairs(target = {}) {
180
170
  return this.matchingCookies(target).map((cookie) => `${cookie.name}=${cookie.value}`);
@@ -190,10 +180,9 @@ export class LambderCookieJar {
190
180
  }
191
181
  /**
192
182
  * The live cookies whose scope reaches this target, in RFC 6265 send
193
- * order. Delegated to tough-cookie whenever the target names a host,
194
- * which is the case worth getting exactly right; an unnamed host falls
195
- * back to every cookie the jar holds, filtered by the rules that do not
196
- * need one and ordered by the same rule.
183
+ * order. Delegated to tough-cookie whenever a host is known, the case
184
+ * worth getting exactly right; with no host, every cookie the jar holds
185
+ * is filtered by the rules that need none and ordered the same way.
197
186
  */
198
187
  matchingCookies(target, includeHttpOnly = true) {
199
188
  const secure = target.secure !== false;
@@ -209,7 +198,7 @@ export class LambderCookieJar {
209
198
  allPaths: target.path === undefined,
210
199
  // tough-cookie returns store order unless asked; RFC 6265
211
200
  // order is what a server reading the first of a repeated
212
- // name actually gets.
201
+ // name gets.
213
202
  sort: true,
214
203
  })
215
204
  .map((cookie) => storedFromCookie(cookie, this.now()))
@@ -218,8 +207,8 @@ export class LambderCookieJar {
218
207
  .filter((cookie) => cookie.expires === undefined || cookie.expires > this.now())
219
208
  // tough-cookie treats a loopback or localhost target as a
220
209
  // secure context and sends Secure cookies to it over http.
221
- // This jar takes `secure: false` at its word in both
222
- // directions, so what it stores and what it sends agree.
210
+ // This jar takes `secure: false` at its word both ways, so
211
+ // what it stores and what it sends agree.
223
212
  .filter((cookie) => secure || !cookie.secure);
224
213
  }
225
214
  return this.list()
@@ -232,9 +221,8 @@ export class LambderCookieJar {
232
221
  return false;
233
222
  return true;
234
223
  })
235
- // The longest path first, as tough-cookie's own comparison does;
236
- // the sort is stable, so equal paths keep the order they were
237
- // stored in, which is the order they were created in.
224
+ // Longest path first, as tough-cookie sorts; the sort is stable,
225
+ // so equal paths keep their creation order.
238
226
  .sort((a, b) => b.path.length - a.path.length);
239
227
  }
240
228
  /** Number of live cookies. */
@@ -3,23 +3,21 @@ import type { LambderCookieJar } from "./LambderCookieJar.js";
3
3
  /**
4
4
  * Makes any transport carry a cookie jar the way a browser carries its
5
5
  * cookies: the jar's cookies ride on every request, the answer's Set-Cookie
6
- * headers land in the jar, and, because a page's script reads the
7
- * non-HttpOnly CSRF cookie the same way, the request's `token` is filled
8
- * from the jar when the caller sent an empty one. This is how a session
9
- * survives between calls where there is no browser: in a Node test through
10
- * lambderHandlerTransport, or in the mock runtime's direct transport.
6
+ * headers land in the jar, and, as a page's script reads the non-HttpOnly
7
+ * CSRF cookie, the request's `token` is filled from the jar when the caller
8
+ * sent an empty one. This is how a session survives between calls without a
9
+ * browser: in a Node test through lambderHandlerTransport, or in the mock
10
+ * runtime's direct transport.
11
11
  *
12
12
  * The jar is only as scoped as the host it is told about. An absolute apiPath
13
13
  * carries one, `host` names one when the path is relative, and a browser's
14
- * caller falls back to its page's: an in-process or mock transport posts to a
15
- * relative path from a runtime with no location, so nothing there says which
16
- * host these cookies belong to. Without any of the three the jar is a single
17
- * host's, refusing Domain cookies it cannot check (see LambderCookieJar), so
18
- * give `host` (or the jar one) to any jar that more than one host answers
19
- * into.
14
+ * caller falls back to its page's; an in-process or mock transport posts to a
15
+ * relative path from a runtime with no location, so nothing there names the
16
+ * host. Without any of the three the jar is a single host's and refuses
17
+ * Domain cookies it cannot check (see LambderCookieJar), so give `host` (or
18
+ * the jar one) to any jar that more than one host answers into.
20
19
  *
21
- * Its own file rather than a passage inside the transport seam: it is a
22
- * transport like its three siblings, and it is the one of them that pulls in
20
+ * Its own file, like its three sibling transports, because it pulls in
23
21
  * tough-cookie, which a bundle that never carries a jar should be able to
24
22
  * drop.
25
23
  */
@@ -3,23 +3,21 @@ import { resolveApiPathTarget } from "./LambderApiTransport.js";
3
3
  /**
4
4
  * Makes any transport carry a cookie jar the way a browser carries its
5
5
  * cookies: the jar's cookies ride on every request, the answer's Set-Cookie
6
- * headers land in the jar, and, because a page's script reads the
7
- * non-HttpOnly CSRF cookie the same way, the request's `token` is filled
8
- * from the jar when the caller sent an empty one. This is how a session
9
- * survives between calls where there is no browser: in a Node test through
10
- * lambderHandlerTransport, or in the mock runtime's direct transport.
6
+ * headers land in the jar, and, as a page's script reads the non-HttpOnly
7
+ * CSRF cookie, the request's `token` is filled from the jar when the caller
8
+ * sent an empty one. This is how a session survives between calls without a
9
+ * browser: in a Node test through lambderHandlerTransport, or in the mock
10
+ * runtime's direct transport.
11
11
  *
12
12
  * The jar is only as scoped as the host it is told about. An absolute apiPath
13
13
  * carries one, `host` names one when the path is relative, and a browser's
14
- * caller falls back to its page's: an in-process or mock transport posts to a
15
- * relative path from a runtime with no location, so nothing there says which
16
- * host these cookies belong to. Without any of the three the jar is a single
17
- * host's, refusing Domain cookies it cannot check (see LambderCookieJar), so
18
- * give `host` (or the jar one) to any jar that more than one host answers
19
- * into.
14
+ * caller falls back to its page's; an in-process or mock transport posts to a
15
+ * relative path from a runtime with no location, so nothing there names the
16
+ * host. Without any of the three the jar is a single host's and refuses
17
+ * Domain cookies it cannot check (see LambderCookieJar), so give `host` (or
18
+ * the jar one) to any jar that more than one host answers into.
20
19
  *
21
- * Its own file rather than a passage inside the transport seam: it is a
22
- * transport like its three siblings, and it is the one of them that pulls in
20
+ * Its own file, like its three sibling transports, because it pulls in
23
21
  * tough-cookie, which a bundle that never carries a jar should be able to
24
22
  * drop.
25
23
  */
@@ -31,23 +29,23 @@ export const lambderCookieJarTransport = (inner, options) => {
31
29
  // through setSessionCookieKey.
32
30
  const csrfCookieKey = options.csrfCookieKey ?? request.csrfCookieKey ?? DEFAULT_SESSION_CSRF_COOKIE_KEY;
33
31
  // Where the call is going. An apiPath that names its own host is a
34
- // fact about this request and outranks both the `host` option and the
35
- // caller's siteHost: the option is the fallback for a relative path,
36
- // where nothing says which host these cookies belong to, and siteHost
37
- // is only the page the caller happens to be on. Read the other way
38
- // round, a transport pinned to `host: "app.example.com"` sent
39
- // app.example.com's session to an absolute cross-origin apiPath.
40
- // siteHost is "" outside a browser, and an empty host would scope
41
- // every cookie to nothing while claiming to scope it.
32
+ // fact about this request and outranks both the `host` option (the
33
+ // fallback for a relative path) and the caller's siteHost (only the
34
+ // page the caller is on). The other way round, a transport pinned to
35
+ // `host: "app.example.com"` would send that host's session to an
36
+ // absolute cross-origin apiPath. siteHost is "" outside a browser, and
37
+ // an empty host would scope every cookie to nothing while claiming to
38
+ // scope it.
42
39
  const target = resolveApiPathTarget(request.apiPath);
43
40
  const cookieScope = {
44
41
  host: target.host ?? options.host ?? (request.siteHost || undefined),
45
42
  path: target.path,
46
43
  ...(target.secure !== undefined ? { secure: target.secure } : {}),
47
44
  };
45
+ const postedToken = request.token || options.jar.get(csrfCookieKey, cookieScope) || "";
48
46
  const answer = await inner({
49
47
  ...request,
50
- token: request.token || options.jar.get(csrfCookieKey, cookieScope) || "",
48
+ token: postedToken,
51
49
  cookies: [...(request.cookies ?? []), ...options.jar.cookiePairs(cookieScope)],
52
50
  });
53
51
  // The same scope the request was made under, so a Set-Cookie is judged
@@ -55,6 +53,9 @@ export const lambderCookieJarTransport = (inner, options) => {
55
53
  // plain-http target is one this jar could never send back.
56
54
  if (answer.setCookies?.length)
57
55
  options.jar.storeSetCookies(answer.setCookies, cookieScope);
58
- return answer;
56
+ // The session lives in the jar, not in document.cookie, so the caller
57
+ // tells a sessionExpired about an older session (a poll sent before a
58
+ // login) by the jar's token, as a page does by its cookie.
59
+ return { ...answer, csrfTokens: { posted: postedToken, held: () => options.jar.get(csrfCookieKey, cookieScope) ?? "" } };
59
60
  };
60
61
  };
@@ -5,10 +5,9 @@
5
5
  * Both give a call a `timeoutMs`, both let the site pass its own AbortSignal,
6
6
  * and both have to answer the same three questions: which signal does the
7
7
  * transport get, has the call already been given up on before it is sent, and
8
- * did the answer arrive after it was given up on. Written twice, the two
9
- * drifted: the browser caller learned not to believe a late answer and the
10
- * invoke caller did not, so a 20ms timeoutMs there reported `ok: true` at
11
- * 300ms and the call site acted on data it had already abandoned.
8
+ * did the answer arrive after it was given up on. One implementation keeps
9
+ * the two from drifting apart: a caller that believed a late answer would
10
+ * report `ok: true` for a call its site had already abandoned.
12
11
  *
13
12
  * The listener on an external signal is removed in detach() rather than left
14
13
  * to `once`: a site's signal usually outlives the call (one controller per
@@ -53,7 +52,8 @@ export declare const createCallAbort: (options: {
53
52
  * the callee runs to completion either way, and what the caller's timeout
54
53
  * buys is its own answer. Used by lambderHandlerTransport and by
55
54
  * LambderInvokeCaller.localTransport, whose in-process calls are the two
56
- * places a signal has nothing to cancel.
55
+ * transports a signal has nothing to cancel, and by the crash reporter's
56
+ * time bound (LambderCrashHandling), where the app's report runs on.
57
57
  *
58
58
  * The listener is detached on either outcome, for the reason createCallAbort
59
59
  * detaches its own.
@@ -5,10 +5,9 @@
5
5
  * Both give a call a `timeoutMs`, both let the site pass its own AbortSignal,
6
6
  * and both have to answer the same three questions: which signal does the
7
7
  * transport get, has the call already been given up on before it is sent, and
8
- * did the answer arrive after it was given up on. Written twice, the two
9
- * drifted: the browser caller learned not to believe a late answer and the
10
- * invoke caller did not, so a 20ms timeoutMs there reported `ok: true` at
11
- * 300ms and the call site acted on data it had already abandoned.
8
+ * did the answer arrive after it was given up on. One implementation keeps
9
+ * the two from drifting apart: a caller that believed a late answer would
10
+ * report `ok: true` for a call its site had already abandoned.
12
11
  *
13
12
  * The listener on an external signal is removed in detach() rather than left
14
13
  * to `once`: a site's signal usually outlives the call (one controller per
@@ -64,7 +63,8 @@ export const createCallAbort = (options) => {
64
63
  * the callee runs to completion either way, and what the caller's timeout
65
64
  * buys is its own answer. Used by lambderHandlerTransport and by
66
65
  * LambderInvokeCaller.localTransport, whose in-process calls are the two
67
- * places a signal has nothing to cancel.
66
+ * transports a signal has nothing to cancel, and by the crash reporter's
67
+ * time bound (LambderCrashHandling), where the app's report runs on.
68
68
  *
69
69
  * The listener is detached on either outcome, for the reason createCallAbort
70
70
  * detaches its own.
@@ -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
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A Content-Disposition header for a file name of any characters: an ASCII
3
+ * `filename` for old clients, and the exact name as UTF-8 in `filename*`
4
+ * (RFC 6266). A character a header cannot carry never reaches it, so a name
5
+ * holding a quote or a line break cannot end the header early.
6
+ */
7
+ export declare const contentDispositionHeader: ({ disposition, fileName }: {
8
+ disposition: "inline" | "attachment";
9
+ fileName?: string;
10
+ }) => string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A Content-Disposition header for a file name of any characters: an ASCII
3
+ * `filename` for old clients, and the exact name as UTF-8 in `filename*`
4
+ * (RFC 6266). A character a header cannot carry never reaches it, so a name
5
+ * holding a quote or a line break cannot end the header early.
6
+ */
7
+ export const contentDispositionHeader = ({ disposition, fileName }) => {
8
+ if (fileName === undefined)
9
+ return disposition;
10
+ const fallback = fileName.replace(/[^\x20-\x7e]|["\\%]/g, "_");
11
+ const encoded = encodeURIComponent(fileName).replace(/['()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
12
+ return `${disposition}; filename="${fallback}"; filename*=UTF-8''${encoded}`;
13
+ };