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.
- package/CHANGELOG.md +1047 -3
- package/README.md +46 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/ContractTypePrinter.d.ts +85 -0
- package/dist/build/ContractTypePrinter.js +402 -0
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +78 -0
- package/dist/build/writeApiContract.js +302 -0
- package/dist/build/writeApiSignatures.d.ts +114 -0
- package/dist/build/writeApiSignatures.js +217 -0
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +11 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +9 -3
- package/dist/client.js +6 -1
- package/dist/core/Lambder.d.ts +143 -79
- package/dist/core/Lambder.js +350 -231
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +25 -8
- package/dist/index.js +13 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +43 -33
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +6 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +98 -53
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
- package/dist/shared/wire/LambderApiRefusal.js +42 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +80 -38
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +23 -25
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +2 -0
- package/package.json +26 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* can never
|
|
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
|
|
32
|
-
* `expires`
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
54
|
-
//
|
|
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
|
|
72
|
-
//
|
|
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
|
-
//
|
|
75
|
-
//
|
|
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
|
|
86
|
+
* carry. One jar is one browser.
|
|
89
87
|
*
|
|
90
|
-
* The rules
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
|
108
|
-
* initiate this", and
|
|
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
|
|
133
|
-
* (RFC 6265 section 5.3 step 6), and so does a Domain
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
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
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
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
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
|
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
|
|
222
|
-
//
|
|
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
|
-
//
|
|
236
|
-
//
|
|
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,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
15
|
-
* relative path from a runtime with no location, so nothing there
|
|
16
|
-
* host
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
15
|
-
* relative path from a runtime with no location, so nothing there
|
|
16
|
-
* host
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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:
|
|
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
|
-
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* is
|
|
16
|
+
* ([2001:db8::1]:443) or a plain host:port; each variant would otherwise be
|
|
17
|
+
* its own counter, which is a limit that does not limit. An unbracketed IPv6
|
|
18
|
+
* address with a port after it cannot be told apart by its text
|
|
19
|
+
* (`2001:db8::1:443` is an address too), so this leaves it alone; the header
|
|
20
|
+
* that writes that form has its port taken off by resolveClientIp, which
|
|
21
|
+
* knows where the value came from. Anything longer than the longest valid
|
|
22
|
+
* address is truncated rather than trusted as a key.
|
|
20
23
|
*/
|
|
21
24
|
export declare const normalizeClientIp: (value: string) => string;
|
|
25
|
+
/** The IPv6 prefix a per-IP rate limit counts by default: the /64 every subscriber holds at least. */
|
|
26
|
+
export declare const DEFAULT_IPV6_RATE_LIMIT_PREFIX = 64;
|
|
27
|
+
/**
|
|
28
|
+
* The caller a per-IP rate limit counts, which is not always the address
|
|
29
|
+
* itself. An IPv4 address is one subscriber. An IPv6 subscriber, a VPS
|
|
30
|
+
* included, holds at least a /64 and may pick any interface id inside it, so
|
|
31
|
+
* one counter per full address is a fresh counter per request for anyone
|
|
32
|
+
* who rotates: the prefix is what is counted (`2001:db8:1:2:0:0:0:0/64`). An
|
|
33
|
+
* IPv4-mapped IPv6 address (::ffff:192.0.2.1) is its IPv4 address, so the
|
|
34
|
+
* two spellings share a counter. A value that is not an address is counted
|
|
35
|
+
* as it is. ctx.ip itself stays the exact address, for logs.
|
|
36
|
+
*/
|
|
37
|
+
export declare const rateLimitSubjectOf: (ip: string, ipv6PrefixLength?: number) => string;
|
|
22
38
|
/**
|
|
23
39
|
* The first trusted header that carries anything, leftmost entry, else the
|
|
24
|
-
* address the gateway observed
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* from anyway.
|
|
40
|
+
* address the gateway observed; a header that always appends the port (see
|
|
41
|
+
* PORT_SUFFIXED_IP_HEADERS) has it taken off. Nothing is trusted by default,
|
|
42
|
+
* and there is no exception for an invoke: the invoke marker is an ordinary
|
|
43
|
+
* request header any HTTP caller can set, so honouring it would let every
|
|
44
|
+
* caller choose its own address. A genuine invoke needs no exception,
|
|
45
|
+
* because the synthesized event carries the end user's address in
|
|
46
|
+
* requestContext.http.sourceIp, which is where `sourceIp` comes from anyway.
|
|
31
47
|
*/
|
|
32
48
|
export declare const resolveClientIp: (lowercasedHeaders: Record<string, string>, sourceIp: string, trustedClientIpHeaders?: readonly string[]) => string;
|
|
@@ -12,13 +12,60 @@
|
|
|
12
12
|
export const LOOPBACK_CLIENT_IP = "127.0.0.1";
|
|
13
13
|
/** Longest textual IPv6 address (39) plus a scope id; beyond this the value is not an address. */
|
|
14
14
|
const MAX_CLIENT_IP_LENGTH = 45;
|
|
15
|
+
/** A dotted IPv4 address as its four octets, or null when the text is not one. */
|
|
16
|
+
const parseIpv4 = (value) => {
|
|
17
|
+
const parts = value.split(".");
|
|
18
|
+
if (parts.length !== 4 || !parts.every((part) => /^\d{1,3}$/.test(part)))
|
|
19
|
+
return null;
|
|
20
|
+
const octets = parts.map(Number);
|
|
21
|
+
return octets.every((octet) => octet <= 255) ? octets : null;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* An IPv6 address as its eight 16-bit groups, or null when the text is not
|
|
25
|
+
* one: every spelling of one address (compressed or not, any case, with an
|
|
26
|
+
* embedded IPv4 tail) parses to the same groups.
|
|
27
|
+
*/
|
|
28
|
+
const parseIpv6 = (value) => {
|
|
29
|
+
// A zone id names the interface an address was reached on, not the address.
|
|
30
|
+
let text = value.split("%")[0];
|
|
31
|
+
const lastColon = text.lastIndexOf(":");
|
|
32
|
+
if (lastColon === -1)
|
|
33
|
+
return null;
|
|
34
|
+
const tail = text.slice(lastColon + 1);
|
|
35
|
+
if (tail.includes(".")) {
|
|
36
|
+
const octets = parseIpv4(tail);
|
|
37
|
+
if (!octets)
|
|
38
|
+
return null;
|
|
39
|
+
text = `${text.slice(0, lastColon + 1)}${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
|
|
40
|
+
}
|
|
41
|
+
const halves = text.split("::");
|
|
42
|
+
if (halves.length > 2)
|
|
43
|
+
return null;
|
|
44
|
+
const groupsOf = (part) => {
|
|
45
|
+
if (part === "")
|
|
46
|
+
return [];
|
|
47
|
+
const groups = part.split(":");
|
|
48
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/i.test(group)) ? groups.map((group) => parseInt(group, 16)) : null;
|
|
49
|
+
};
|
|
50
|
+
const head = groupsOf(halves[0]);
|
|
51
|
+
const rest = halves.length === 2 ? groupsOf(halves[1]) : [];
|
|
52
|
+
if (!head || !rest)
|
|
53
|
+
return null;
|
|
54
|
+
if (halves.length === 1)
|
|
55
|
+
return head.length === 8 ? head : null;
|
|
56
|
+
const missing = 8 - head.length - rest.length;
|
|
57
|
+
return missing >= 1 ? [...head, ...new Array(missing).fill(0), ...rest] : null;
|
|
58
|
+
};
|
|
15
59
|
/**
|
|
16
60
|
* One textual form per address, so a rate-limit counter cannot be split.
|
|
17
61
|
* A forwarding proxy may write the RFC 7239 bracket-and-port form
|
|
18
|
-
* ([2001:db8::1]:443) or a plain host:port
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* is
|
|
62
|
+
* ([2001:db8::1]:443) or a plain host:port; each variant would otherwise be
|
|
63
|
+
* its own counter, which is a limit that does not limit. An unbracketed IPv6
|
|
64
|
+
* address with a port after it cannot be told apart by its text
|
|
65
|
+
* (`2001:db8::1:443` is an address too), so this leaves it alone; the header
|
|
66
|
+
* that writes that form has its port taken off by resolveClientIp, which
|
|
67
|
+
* knows where the value came from. Anything longer than the longest valid
|
|
68
|
+
* address is truncated rather than trusted as a key.
|
|
22
69
|
*/
|
|
23
70
|
export const normalizeClientIp = (value) => {
|
|
24
71
|
let ip = value.trim();
|
|
@@ -35,22 +82,58 @@ export const normalizeClientIp = (value) => {
|
|
|
35
82
|
}
|
|
36
83
|
return ip.toLowerCase().slice(0, MAX_CLIENT_IP_LENGTH);
|
|
37
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Headers whose value always ends in `:port`, whatever the address before
|
|
87
|
+
* it: CloudFront-Viewer-Address writes `192.0.2.1:443` and
|
|
88
|
+
* `2001:db8::1:443` alike, with no brackets. The last colon-separated
|
|
89
|
+
* segment of such a value is the port by definition. Read from the text
|
|
90
|
+
* instead, a compressed IPv6 address with its port on still parses, the port
|
|
91
|
+
* becomes its last group, and the /64 a per-IP limit counts moves with the
|
|
92
|
+
* interface id the caller picks.
|
|
93
|
+
*/
|
|
94
|
+
const PORT_SUFFIXED_IP_HEADERS = new Set(["cloudfront-viewer-address"]);
|
|
95
|
+
/** The IPv6 prefix a per-IP rate limit counts by default: the /64 every subscriber holds at least. */
|
|
96
|
+
export const DEFAULT_IPV6_RATE_LIMIT_PREFIX = 64;
|
|
97
|
+
/**
|
|
98
|
+
* The caller a per-IP rate limit counts, which is not always the address
|
|
99
|
+
* itself. An IPv4 address is one subscriber. An IPv6 subscriber, a VPS
|
|
100
|
+
* included, holds at least a /64 and may pick any interface id inside it, so
|
|
101
|
+
* one counter per full address is a fresh counter per request for anyone
|
|
102
|
+
* who rotates: the prefix is what is counted (`2001:db8:1:2:0:0:0:0/64`). An
|
|
103
|
+
* IPv4-mapped IPv6 address (::ffff:192.0.2.1) is its IPv4 address, so the
|
|
104
|
+
* two spellings share a counter. A value that is not an address is counted
|
|
105
|
+
* as it is. ctx.ip itself stays the exact address, for logs.
|
|
106
|
+
*/
|
|
107
|
+
export const rateLimitSubjectOf = (ip, ipv6PrefixLength = DEFAULT_IPV6_RATE_LIMIT_PREFIX) => {
|
|
108
|
+
const groups = parseIpv6(ip);
|
|
109
|
+
if (!groups)
|
|
110
|
+
return ip;
|
|
111
|
+
if (groups.slice(0, 5).every((group) => group === 0) && groups[5] === 0xffff) {
|
|
112
|
+
return `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
|
|
113
|
+
}
|
|
114
|
+
const masked = groups.map((group, index) => {
|
|
115
|
+
const kept = Math.max(0, Math.min(16, ipv6PrefixLength - index * 16));
|
|
116
|
+
return kept === 0 ? 0 : group & ((0xffff << (16 - kept)) & 0xffff);
|
|
117
|
+
});
|
|
118
|
+
return `${masked.map((group) => group.toString(16)).join(":")}/${ipv6PrefixLength}`;
|
|
119
|
+
};
|
|
38
120
|
/**
|
|
39
121
|
* The first trusted header that carries anything, leftmost entry, else the
|
|
40
|
-
* address the gateway observed
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* from anyway.
|
|
122
|
+
* address the gateway observed; a header that always appends the port (see
|
|
123
|
+
* PORT_SUFFIXED_IP_HEADERS) has it taken off. Nothing is trusted by default,
|
|
124
|
+
* and there is no exception for an invoke: the invoke marker is an ordinary
|
|
125
|
+
* request header any HTTP caller can set, so honouring it would let every
|
|
126
|
+
* caller choose its own address. A genuine invoke needs no exception,
|
|
127
|
+
* because the synthesized event carries the end user's address in
|
|
128
|
+
* requestContext.http.sourceIp, which is where `sourceIp` comes from anyway.
|
|
47
129
|
*/
|
|
48
130
|
export const resolveClientIp = (lowercasedHeaders, sourceIp, trustedClientIpHeaders = []) => {
|
|
49
131
|
for (const name of trustedClientIpHeaders) {
|
|
50
|
-
const
|
|
132
|
+
const header = name.toLowerCase();
|
|
133
|
+
const value = lowercasedHeaders[header];
|
|
51
134
|
const first = value ? (value.split(",")[0] ?? "").trim() : "";
|
|
52
135
|
if (first)
|
|
53
|
-
return normalizeClientIp(first);
|
|
136
|
+
return normalizeClientIp(PORT_SUFFIXED_IP_HEADERS.has(header) ? first.replace(/:\d+$/, "") : first);
|
|
54
137
|
}
|
|
55
138
|
return normalizeClientIp(sourceIp) || "";
|
|
56
139
|
};
|
|
@@ -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
|
+
};
|