lambder 6.0.2 → 7.0.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 (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +21 -19
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The cookie jar a transport carries (LambderCookieJar), and the Set-Cookie
3
+ * parsing behind it. tough-cookie is reached from this module and no other, so
4
+ * a bundle that never carries a jar drops it.
5
+ */
6
+ import { Cookie, CookieJar as ToughCookieJar, defaultPath, pathMatch } from "tough-cookie";
7
+ /**
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.
13
+ */
14
+ const UNNAMED_JAR_HOST = "lambder-cookie-jar.invalid";
15
+ /** The host without its port, lowercased: "App.Test:3000" is "app.test", "[::1]:8080" is "[::1]". */
16
+ const normalizeHost = (host) => {
17
+ const lowered = host.trim().toLowerCase();
18
+ // An IPv6 literal is bracketed and full of colons, so only what follows
19
+ // the closing bracket can be a port; splitting on ":" would leave "[".
20
+ if (lowered.startsWith("[")) {
21
+ const close = lowered.indexOf("]");
22
+ return close === -1 ? lowered : lowered.slice(0, close + 1);
23
+ }
24
+ return lowered.split(":")[0] ?? lowered;
25
+ };
26
+ /** The URL tough-cookie works in terms of. https unless the target says it speaks plain http, which is what decides a Secure cookie. */
27
+ const urlFor = (host, path, secure) => `${secure ? "https" : "http"}://${host}${path && path.startsWith("/") ? path : "/"}`;
28
+ /**
29
+ * When a cookie actually dies, as an absolute moment.
30
+ *
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.
37
+ */
38
+ const cookieExpiryAt = (cookie, now) => {
39
+ if (typeof cookie.maxAge === "number") {
40
+ const receivedAt = cookie.creation instanceof Date ? cookie.creation.getTime() : now;
41
+ return receivedAt + cookie.maxAge * 1000;
42
+ }
43
+ return cookie.expires instanceof Date ? cookie.expires.getTime() : undefined;
44
+ };
45
+ const storedFromCookie = (cookie, now) => {
46
+ const hostOnly = cookie.hostOnly === true;
47
+ const domain = typeof cookie.domain === "string" ? cookie.domain : undefined;
48
+ const expiryAt = cookieExpiryAt(cookie, now);
49
+ return {
50
+ name: cookie.key ?? "",
51
+ value: cookie.value ?? "",
52
+ 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.
55
+ ...(hostOnly && domain !== undefined && domain !== UNNAMED_JAR_HOST ? { host: domain } : {}),
56
+ path: typeof cookie.path === "string" ? cookie.path : "/",
57
+ expires: expiryAt,
58
+ httpOnly: cookie.httpOnly === true,
59
+ secure: cookie.secure === true,
60
+ };
61
+ };
62
+ /**
63
+ * One Set-Cookie header value, read the way a browser reads it. `requestPath`
64
+ * is the path the answer came from, which decides the default Path. Returns
65
+ * null for a header no browser would keep.
66
+ */
67
+ export const parseSetCookie = (header, now, requestPath) => {
68
+ const parsed = Cookie.parse(header, { loose: false });
69
+ if (!parsed)
70
+ 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.
73
+ 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.
76
+ return {
77
+ ...storedFromCookie(parsed, now),
78
+ // Cookie.parse leaves an absent Path absent; the default-path is the
79
+ // sending path's directory, which only the caller knows.
80
+ path: parsed.path ?? defaultPath(requestPath ?? "/"),
81
+ };
82
+ };
83
+ /**
84
+ * A browser's cookie storage, for transports that have no browser: the
85
+ * in-process handler transport in a Node test, and the mock runtime's direct
86
+ * transport. It stores what an answer's Set-Cookie headers set, honours their
87
+ * expiry and deletion, and hands back the Cookie pairs the next request should
88
+ * carry. One jar is one browser; two jars are two.
89
+ *
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.
97
+ *
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.
106
+ *
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.
110
+ */
111
+ export class LambderCookieJar {
112
+ // prefixSecurity "silent" drops a __Host-/__Secure- cookie that breaks its
113
+ // own prefix rules instead of throwing, which is what a browser does.
114
+ jar = new ToughCookieJar(undefined, { prefixSecurity: "silent", allowSpecialUseDomain: true });
115
+ now;
116
+ host;
117
+ /** `host` is the host this jar is the browser of: the sender of every answer and the target of every request that names none. */
118
+ constructor(options = {}) {
119
+ this.now = options.now ?? (() => Date.now());
120
+ this.host = options.host === undefined ? undefined : normalizeHost(options.host);
121
+ }
122
+ /** The host a call is about, or the stand-in when neither the call nor the jar names one. */
123
+ hostFor(given) {
124
+ return given !== undefined ? normalizeHost(given) : this.host ?? UNNAMED_JAR_HOST;
125
+ }
126
+ /**
127
+ * Applies Set-Cookie header values as a browser would: stores, replaces,
128
+ * and deletes on an expiry in the past. `request` says where the answer
129
+ * came from. Its `host` is the sending host, which every Domain is checked
130
+ * against, and its `path` is the default Path of a cookie that names none.
131
+ *
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.
136
+ */
137
+ 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.
142
+ const secure = request.secure !== false;
143
+ const url = urlFor(this.hostFor(request.host), request.path, secure);
144
+ const now = new Date(this.now());
145
+ 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.
151
+ if (!secure && Cookie.parse(header, { loose: false })?.secure)
152
+ continue;
153
+ // ignoreError: a cookie a browser would refuse is one this jar
154
+ // refuses, silently, rather than failing the call that carried it.
155
+ this.jar.setCookieSync(header, url, { http: true, now, ignoreError: true });
156
+ }
157
+ }
158
+ /** Every live cookie. */
159
+ list() {
160
+ const now = this.now();
161
+ return (this.jar.serializeSync()?.cookies ?? [])
162
+ .map((serialized) => Cookie.fromJSON(serialized))
163
+ .filter((cookie) => cookie !== undefined)
164
+ .map((cookie) => storedFromCookie(cookie, now))
165
+ .filter((cookie) => cookie.expires === undefined || cookie.expires > now);
166
+ }
167
+ /**
168
+ * 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.
173
+ *
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.
178
+ */
179
+ cookiePairs(target = {}) {
180
+ return this.matchingCookies(target).map((cookie) => `${cookie.name}=${cookie.value}`);
181
+ }
182
+ /**
183
+ * One cookie's value as a page's script would read it: HttpOnly cookies
184
+ * are invisible unless asked for, which is how the transport fills in the
185
+ * CSRF token the caller would have read from document.cookie.
186
+ */
187
+ get(name, options = {}) {
188
+ return this.matchingCookies(options, options.includeHttpOnly === true)
189
+ .find((cookie) => cookie.name === name)?.value;
190
+ }
191
+ /**
192
+ * 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.
197
+ */
198
+ matchingCookies(target, includeHttpOnly = true) {
199
+ const secure = target.secure !== false;
200
+ if (target.host !== undefined || this.host !== undefined) {
201
+ const url = urlFor(this.hostFor(target.host), target.path, secure);
202
+ return this.jar
203
+ .getCookiesSync(url, {
204
+ // http: false is a script reading document.cookie, which
205
+ // is exactly what hides an HttpOnly cookie.
206
+ http: includeHttpOnly,
207
+ // A target that named no path is asking about the jar, not
208
+ // about one endpoint.
209
+ allPaths: target.path === undefined,
210
+ // tough-cookie returns store order unless asked; RFC 6265
211
+ // order is what a server reading the first of a repeated
212
+ // name actually gets.
213
+ sort: true,
214
+ })
215
+ .map((cookie) => storedFromCookie(cookie, this.now()))
216
+ // getCookiesSync expires against the real clock; a jar given
217
+ // an injected one is usually a test moving time forward.
218
+ .filter((cookie) => cookie.expires === undefined || cookie.expires > this.now())
219
+ // tough-cookie treats a loopback or localhost target as a
220
+ // 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.
223
+ .filter((cookie) => secure || !cookie.secure);
224
+ }
225
+ return this.list()
226
+ .filter((cookie) => {
227
+ if (cookie.httpOnly && !includeHttpOnly)
228
+ return false;
229
+ if (cookie.secure && !secure)
230
+ return false;
231
+ if (target.path !== undefined && !pathMatch(target.path, cookie.path))
232
+ return false;
233
+ return true;
234
+ })
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.
238
+ .sort((a, b) => b.path.length - a.path.length);
239
+ }
240
+ /** Number of live cookies. */
241
+ get size() { return this.list().length; }
242
+ /** Forgets every cookie: the browser's storage cleared. */
243
+ clear() {
244
+ this.jar.removeAllCookiesSync();
245
+ }
246
+ }
@@ -0,0 +1,30 @@
1
+ import { type LambderApiTransport } from "./LambderApiTransport.js";
2
+ import type { LambderCookieJar } from "./LambderCookieJar.js";
3
+ /**
4
+ * Makes any transport carry a cookie jar the way a browser carries its
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.
11
+ *
12
+ * The jar is only as scoped as the host it is told about. An absolute apiPath
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.
20
+ *
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
23
+ * tough-cookie, which a bundle that never carries a jar should be able to
24
+ * drop.
25
+ */
26
+ export declare const lambderCookieJarTransport: (inner: LambderApiTransport, options: {
27
+ jar: LambderCookieJar;
28
+ csrfCookieKey?: string;
29
+ host?: string;
30
+ }) => LambderApiTransport;
@@ -0,0 +1,60 @@
1
+ import { DEFAULT_SESSION_CSRF_COOKIE_KEY } from "../wire/LambderSessionCookieNames.js";
2
+ import { resolveApiPathTarget } from "./LambderApiTransport.js";
3
+ /**
4
+ * Makes any transport carry a cookie jar the way a browser carries its
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.
11
+ *
12
+ * The jar is only as scoped as the host it is told about. An absolute apiPath
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.
20
+ *
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
23
+ * tough-cookie, which a bundle that never carries a jar should be able to
24
+ * drop.
25
+ */
26
+ export const lambderCookieJarTransport = (inner, options) => {
27
+ return async (request) => {
28
+ // The caller's own name wins over the default, and an explicit option
29
+ // over both: reading the default name regardless would hand every
30
+ // session call an empty token whenever a caller was told a custom one
31
+ // through setSessionCookieKey.
32
+ const csrfCookieKey = options.csrfCookieKey ?? request.csrfCookieKey ?? DEFAULT_SESSION_CSRF_COOKIE_KEY;
33
+ // 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.
42
+ const target = resolveApiPathTarget(request.apiPath);
43
+ const cookieScope = {
44
+ host: target.host ?? options.host ?? (request.siteHost || undefined),
45
+ path: target.path,
46
+ ...(target.secure !== undefined ? { secure: target.secure } : {}),
47
+ };
48
+ const answer = await inner({
49
+ ...request,
50
+ token: request.token || options.jar.get(csrfCookieKey, cookieScope) || "",
51
+ cookies: [...(request.cookies ?? []), ...options.jar.cookiePairs(cookieScope)],
52
+ });
53
+ // The same scope the request was made under, so a Set-Cookie is judged
54
+ // against the channel it actually arrived on: a Secure cookie from a
55
+ // plain-http target is one this jar could never send back.
56
+ if (answer.setCookies?.length)
57
+ options.jar.storeSetCookies(answer.setCookies, cookieScope);
58
+ return answer;
59
+ };
60
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Base64 in both directions where Buffer may not exist (the browser caller,
3
+ * the mock runtime in a page). Buffer where there is one, since it is much
4
+ * faster; otherwise the platform's atob/btoa, chunked on the way in so a
5
+ * large payload cannot overflow the argument list of String.fromCharCode.
6
+ */
7
+ export declare const bytesToBase64: (bytes: Uint8Array) => string;
8
+ export declare const base64ToBytes: (base64: string) => Uint8Array;
9
+ /** The base64 of UTF-8 text back to the text. */
10
+ export declare const base64ToText: (base64: string) => string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Base64 in both directions where Buffer may not exist (the browser caller,
3
+ * the mock runtime in a page). Buffer where there is one, since it is much
4
+ * faster; otherwise the platform's atob/btoa, chunked on the way in so a
5
+ * large payload cannot overflow the argument list of String.fromCharCode.
6
+ */
7
+ export const bytesToBase64 = (bytes) => {
8
+ if (typeof Buffer !== "undefined")
9
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
10
+ const chunkSize = 0x8000;
11
+ let binary = "";
12
+ for (let i = 0; i < bytes.length; i += chunkSize) {
13
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
14
+ }
15
+ return btoa(binary);
16
+ };
17
+ export const base64ToBytes = (base64) => {
18
+ if (typeof Buffer !== "undefined")
19
+ return Buffer.from(base64, "base64");
20
+ const binary = atob(base64);
21
+ const bytes = new Uint8Array(binary.length);
22
+ for (let i = 0; i < binary.length; i += 1)
23
+ bytes[i] = binary.charCodeAt(i);
24
+ return bytes;
25
+ };
26
+ /** The base64 of UTF-8 text back to the text. */
27
+ export const base64ToText = (base64) => new TextDecoder().decode(base64ToBytes(base64));
@@ -0,0 +1,62 @@
1
+ /**
2
+ * One call's abort wiring, shared by LambderCaller (a browser over fetch) and
3
+ * LambderInvokeCaller (a server over a Lambda invoke).
4
+ *
5
+ * Both give a call a `timeoutMs`, both let the site pass its own AbortSignal,
6
+ * and both have to answer the same three questions: which signal does the
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.
12
+ *
13
+ * The listener on an external signal is removed in detach() rather than left
14
+ * to `once`: a site's signal usually outlives the call (one controller per
15
+ * page, per view, per request), so a listener per call would accumulate on it
16
+ * for as long as the signal lives.
17
+ */
18
+ /** How an abandoned call is reported: its own timeout fired, or the site's signal did, which is a `network` failure like any other abort. */
19
+ type LambderCallAbortReason = "timeout" | "network";
20
+ /** The reason and the error to report for a call that was given up on. */
21
+ type LambderCallAbortFailure = {
22
+ reason: LambderCallAbortReason;
23
+ error: Error;
24
+ };
25
+ /**
26
+ * Where the abort was noticed, which is all that differs between the two
27
+ * checks: nothing was sent, or something came back too late to be used.
28
+ */
29
+ export type LambderCallAbortStage = "beforeSending" | "afterAnswering";
30
+ type LambderCallAbort = {
31
+ /** What the transport is handed: the chained signal when a timeout is set, the site's own otherwise, and nothing when there is neither. */
32
+ signal: AbortSignal | undefined;
33
+ /** Whether this call's own timeout is what aborted it, rather than the site's signal. */
34
+ timedOut: () => boolean;
35
+ /**
36
+ * The failure to report, or null while the call still stands. Run before
37
+ * handing the request to the transport (a call already abandoned should
38
+ * not reach it) and again after the transport resolves, because honouring
39
+ * `request.signal` is the transport's obligation and not every transport
40
+ * does: an answer that arrives after the abort is not a success.
41
+ */
42
+ abortFailure: (stage: LambderCallAbortStage) => LambderCallAbortFailure | null;
43
+ /** Clears the timer and lets go of the external signal. Belongs in a finally. */
44
+ detach: () => void;
45
+ };
46
+ export declare const createCallAbort: (options: {
47
+ timeoutMs?: number;
48
+ signal?: AbortSignal;
49
+ }) => LambderCallAbort;
50
+ /**
51
+ * Ends the wait on work that cannot be cancelled, which is what honouring
52
+ * `request.signal` means for a transport running a handler in this process:
53
+ * the callee runs to completion either way, and what the caller's timeout
54
+ * buys is its own answer. Used by lambderHandlerTransport and by
55
+ * LambderInvokeCaller.localTransport, whose in-process calls are the two
56
+ * places a signal has nothing to cancel.
57
+ *
58
+ * The listener is detached on either outcome, for the reason createCallAbort
59
+ * detaches its own.
60
+ */
61
+ export declare const stopWaitingWhenAborted: <T>(pending: Promise<T>, signal: AbortSignal | undefined) => Promise<T>;
62
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * One call's abort wiring, shared by LambderCaller (a browser over fetch) and
3
+ * LambderInvokeCaller (a server over a Lambda invoke).
4
+ *
5
+ * Both give a call a `timeoutMs`, both let the site pass its own AbortSignal,
6
+ * and both have to answer the same three questions: which signal does the
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.
12
+ *
13
+ * The listener on an external signal is removed in detach() rather than left
14
+ * to `once`: a site's signal usually outlives the call (one controller per
15
+ * page, per view, per request), so a listener per call would accumulate on it
16
+ * for as long as the signal lives.
17
+ */
18
+ export const createCallAbort = (options) => {
19
+ const { timeoutMs, signal: external } = options;
20
+ let timedOut = false;
21
+ let signal = external;
22
+ let timeoutId;
23
+ let detachExternal;
24
+ if (timeoutMs !== undefined) {
25
+ // The timeout gets its own controller chained to the site's signal, so
26
+ // either source aborts the call and only this one knows which did.
27
+ const controller = new AbortController();
28
+ if (external) {
29
+ if (external.aborted) {
30
+ controller.abort(external.reason);
31
+ }
32
+ else {
33
+ const forwardAbort = () => controller.abort(external.reason);
34
+ external.addEventListener("abort", forwardAbort, { once: true });
35
+ detachExternal = () => external.removeEventListener("abort", forwardAbort);
36
+ }
37
+ }
38
+ timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
39
+ signal = controller.signal;
40
+ }
41
+ return {
42
+ signal,
43
+ timedOut: () => timedOut,
44
+ abortFailure: (stage) => {
45
+ if (!signal?.aborted)
46
+ return null;
47
+ const detail = stage === "beforeSending"
48
+ ? "the call was given up on before it was sent"
49
+ : "the answer arrived too late to be used";
50
+ return timedOut
51
+ ? { reason: "timeout", error: new Error(`Request timed out after ${timeoutMs}ms; ${detail}.`) }
52
+ : { reason: "network", error: new Error(`Request aborted; ${detail}.`) };
53
+ },
54
+ detach: () => {
55
+ if (timeoutId !== undefined)
56
+ clearTimeout(timeoutId);
57
+ detachExternal?.();
58
+ },
59
+ };
60
+ };
61
+ /**
62
+ * Ends the wait on work that cannot be cancelled, which is what honouring
63
+ * `request.signal` means for a transport running a handler in this process:
64
+ * the callee runs to completion either way, and what the caller's timeout
65
+ * buys is its own answer. Used by lambderHandlerTransport and by
66
+ * LambderInvokeCaller.localTransport, whose in-process calls are the two
67
+ * places a signal has nothing to cancel.
68
+ *
69
+ * The listener is detached on either outcome, for the reason createCallAbort
70
+ * detaches its own.
71
+ */
72
+ export const stopWaitingWhenAborted = (pending, signal) => {
73
+ if (!signal)
74
+ return pending;
75
+ return new Promise((resolve, reject) => {
76
+ const onAbort = () => reject(signal.reason);
77
+ signal.addEventListener("abort", onAbort, { once: true });
78
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
79
+ });
80
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The client address as one textual form, and the rule for which header may
3
+ * name it. Shared by every adapter that builds a request (the Lambda server,
4
+ * the mock's invoke transport), so a `per: "ip"` rate limit keys the same
5
+ * address the same way whichever runtime served the call.
6
+ */
7
+ /**
8
+ * The loopback address an in-process caller is given when nothing names a
9
+ * client: the handler transport and the mock runtime both default to it, so
10
+ * a per-IP rate limit exercised in a test keys the same way on both.
11
+ */
12
+ export declare const LOOPBACK_CLIENT_IP = "127.0.0.1";
13
+ /**
14
+ * One textual form per address, so a rate-limit counter cannot be split.
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.
20
+ */
21
+ export declare const normalizeClientIp: (value: string) => string;
22
+ /**
23
+ * 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.
31
+ */
32
+ export declare const resolveClientIp: (lowercasedHeaders: Record<string, string>, sourceIp: string, trustedClientIpHeaders?: readonly string[]) => string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The client address as one textual form, and the rule for which header may
3
+ * name it. Shared by every adapter that builds a request (the Lambda server,
4
+ * the mock's invoke transport), so a `per: "ip"` rate limit keys the same
5
+ * address the same way whichever runtime served the call.
6
+ */
7
+ /**
8
+ * The loopback address an in-process caller is given when nothing names a
9
+ * client: the handler transport and the mock runtime both default to it, so
10
+ * a per-IP rate limit exercised in a test keys the same way on both.
11
+ */
12
+ export const LOOPBACK_CLIENT_IP = "127.0.0.1";
13
+ /** Longest textual IPv6 address (39) plus a scope id; beyond this the value is not an address. */
14
+ const MAX_CLIENT_IP_LENGTH = 45;
15
+ /**
16
+ * One textual form per address, so a rate-limit counter cannot be split.
17
+ * 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.
22
+ */
23
+ export const normalizeClientIp = (value) => {
24
+ let ip = value.trim();
25
+ if (ip.startsWith("[")) {
26
+ // [v6] or [v6]:port
27
+ const close = ip.indexOf("]");
28
+ if (close > 0)
29
+ ip = ip.slice(1, close);
30
+ }
31
+ else if (ip.split(":").length === 2) {
32
+ // host:port, which only an IPv4 address or a hostname can be: a bare
33
+ // IPv6 address always carries more than one colon.
34
+ ip = ip.slice(0, ip.indexOf(":"));
35
+ }
36
+ return ip.toLowerCase().slice(0, MAX_CLIENT_IP_LENGTH);
37
+ };
38
+ /**
39
+ * 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.
47
+ */
48
+ export const resolveClientIp = (lowercasedHeaders, sourceIp, trustedClientIpHeaders = []) => {
49
+ for (const name of trustedClientIpHeaders) {
50
+ const value = lowercasedHeaders[name.toLowerCase()];
51
+ const first = value ? (value.split(",")[0] ?? "").trim() : "";
52
+ if (first)
53
+ return normalizeClientIp(first);
54
+ }
55
+ return normalizeClientIp(sourceIp) || "";
56
+ };