lambder 7.3.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +933 -3
  2. package/README.md +41 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +140 -75
  41. package/dist/core/Lambder.js +347 -227
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +21 -7
  51. package/dist/core/LambderFiles.js +62 -34
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  73. package/dist/invoke/LambderLambdaEvent.js +40 -22
  74. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  75. package/dist/invoke/lambderHandlerTransport.js +15 -18
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +1 -1
  93. package/dist/mock.js +2 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +124 -46
  99. package/dist/session/LambderSessionManager.js +262 -137
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  134. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  135. package/dist/shared/util/boundKeyField.d.ts +20 -0
  136. package/dist/shared/util/boundKeyField.js +34 -0
  137. package/dist/shared/util/canonicalJson.d.ts +11 -0
  138. package/dist/shared/util/canonicalJson.js +28 -0
  139. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  140. package/dist/shared/util/joinKeyFields.js +22 -0
  141. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  142. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  143. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  144. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  145. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  146. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  147. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  148. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  149. package/dist/shared/wire/LambderApiSignature.js +16 -19
  150. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  151. package/dist/shared/wire/LambderCallOptions.js +9 -11
  152. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  153. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  154. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  155. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  156. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  157. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  158. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  159. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  160. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  161. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  162. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  163. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  164. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  165. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  166. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  167. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  168. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  169. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  170. package/dist/stores/LambderCacheFiller.js +119 -0
  171. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  172. package/dist/stores/LambderCacheKeys.js +54 -0
  173. package/dist/stores/LambderCacheValues.d.ts +45 -0
  174. package/dist/stores/LambderCacheValues.js +74 -0
  175. package/dist/stores/LambderDdbCache.d.ts +121 -56
  176. package/dist/stores/LambderDdbCache.js +528 -225
  177. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  178. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  179. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  180. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  181. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  182. package/dist/stores/LambderDdbSdk.js +79 -33
  183. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  184. package/dist/stores/LambderDdbSessionStore.js +119 -47
  185. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  186. package/dist/stores/LambderHttpFileSource.js +15 -13
  187. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  188. package/dist/stores/LambderMemoryCache.js +113 -0
  189. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  190. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  191. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  192. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  193. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  194. package/dist/stores/LambderMemorySessionStore.js +38 -19
  195. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  196. package/dist/stores/LambderS3FileSource.js +12 -7
  197. package/dist/testing/LambderTestApp.d.ts +21 -23
  198. package/dist/testing/LambderTestApp.js +22 -24
  199. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  200. package/dist/testing/LambderTestVisitor.js +15 -15
  201. package/dist/testing.d.ts +1 -0
  202. package/dist/testing.js +1 -0
  203. package/package.json +12 -3
  204. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  205. package/dist/api/LambderApiPolicyEngine.js +0 -85
  206. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  207. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -16,9 +16,9 @@ export type LambderApiTransportRequest = {
16
16
  token: string;
17
17
  /**
18
18
  * The cookie name the caller reads that token from. A transport that
19
- * fills the token in itself (the cookie-jar decorator, where there is no
20
- * document to read) needs the same name, and taking it from the caller is
21
- * what keeps the two from being configured apart.
19
+ * fills the token in itself (the cookie-jar decorator, with no document
20
+ * to read) needs the same name; taking it from the caller keeps the two
21
+ * from being configured apart.
22
22
  */
23
23
  csrfCookieKey?: string;
24
24
  siteHost: string;
@@ -36,10 +36,9 @@ export type LambderApiTransportRequest = {
36
36
  };
37
37
  /**
38
38
  * Why a transport could not deliver a call. `network` is the default reading
39
- * of a rejection: nothing came back. `protocol` says the call reached the
40
- * callee and no answer came of it, either because what came back was not one
41
- * or because the callee threw instead of answering, which is a server or
42
- * wiring fault and should not be reported to a developer as flaky
39
+ * of a rejection: nothing came back. `protocol` means the call reached the
40
+ * callee but produced no answer (what came back was not one, or the callee
41
+ * threw): a server or wiring fault, not to be reported as flaky
43
42
  * connectivity. `timeout` belongs to the caller, which knows whether its own
44
43
  * abort fired.
45
44
  */
@@ -60,27 +59,28 @@ export declare class LambderTransportFailure extends Error {
60
59
  export declare const isLambderTransportFailure: (err: unknown) => err is LambderTransportFailure;
61
60
  /**
62
61
  * Delivers one call and hands back the answer in the accessor form
63
- * resolveApiOutcome() reads. What a transport owes its caller, since nothing
64
- * but this contract stands between a call and a wrong outcome:
62
+ * resolveApiOutcome() reads. What a transport owes its caller:
65
63
  *
66
64
  * - **Any HTTP status is an answer.** A 4xx or 5xx resolves, status and body
67
65
  * included, because resolveApiOutcome() is the one place that reads what a
68
- * status means. A transport that rejects on a status throws away the
69
- * envelope a refusal, a validation failure or a crash arrived in.
66
+ * status means. Rejecting on a status would throw away the envelope a
67
+ * refusal, a validation failure or a crash arrived in.
70
68
  * - **A rejection is a transport failure.** The caller reports it as
71
- * `network` unless the transport threw a LambderTransportFailure naming
72
- * another reason, or `timeout` when the caller's own abort fired. That is
73
- * the channel for the real cause too: a LambderTransportFailure keeps it as
74
- * `cause`, where the caller's `outcome.error` carries it.
69
+ * `network`, as `timeout` when its own abort fired, or as the reason a
70
+ * thrown LambderTransportFailure names. That failure's `cause` carries the
71
+ * real error through to the caller's `outcome.error`.
75
72
  * - **`request.signal` must be honoured**, by rejecting as soon as it aborts.
76
- * It is the only thing that makes the caller's `timeoutMs` and its per-call
77
- * `signal` mean anything: a transport that ignores it leaves a call waiting
78
- * for as long as the callee takes, whatever the caller asked for. Work
79
- * already begun need not be cancellable (an in-process handler is not); the
80
- * obligation is to stop waiting, not to stop the callee.
73
+ * Without that, the caller's `timeoutMs` and per-call `signal` mean
74
+ * nothing and a call waits as long as the callee takes. Work already begun
75
+ * need not be cancellable (an in-process handler is not); the obligation
76
+ * is to stop waiting, not to stop the callee.
81
77
  * - **Timeouts and retries belong to the caller.** A transport starts no
82
78
  * clock of its own and retries nothing, so one call is one delivery
83
79
  * attempt and an idempotency key means what it says.
80
+ * - **A transport that keeps the session's cookies itself reports its CSRF
81
+ * tokens** on the answer (`csrfTokens`), the one it posted and a read of the
82
+ * one it holds, since the caller otherwise judges a sessionExpired by
83
+ * document.cookie, which such a transport never writes.
84
84
  *
85
85
  * Four transports ship: fetch (lambderFetchTransport, the browser default),
86
86
  * an in-process Lambder handler (lambderHandlerTransport, for tests), the
@@ -89,16 +89,16 @@ export declare const isLambderTransportFailure: (err: unknown) => err is Lambder
89
89
  */
90
90
  export type LambderApiTransport = (request: LambderApiTransportRequest) => Promise<LambderApiHttpAnswer>;
91
91
  /**
92
- * The fields of the request envelope, in the order they go on the wire: the
93
- * one statement of what a call sends, for every sender there is.
92
+ * The fields of the request envelope, in wire order: the one statement of
93
+ * what a call sends, for every sender.
94
94
  *
95
95
  * Two senders write it. A transport hands the payload over as a value
96
96
  * (buildTransportEnvelope, below); LambderInvokeCaller has already serialized
97
- * its payload to decide whether to compress it, and splices that JSON onto the
98
- * end rather than parsing and stringifying it a second time
99
- * (buildEnvelopeJson, in invoke/LambderLambdaEvent.ts). The splice sits on top
100
- * of this function precisely so that a new envelope field cannot be added to
101
- * one sender and missed by the other, which nothing on the wire would catch.
97
+ * its payload to decide on compression, and splices that JSON onto the end
98
+ * rather than parsing and stringifying it again (buildEnvelopeJson, in
99
+ * invoke/LambderLambdaEvent.ts). The splice builds on this function so a new
100
+ * envelope field cannot reach one sender and miss the other, which nothing on
101
+ * the wire would catch.
102
102
  */
103
103
  export declare const buildEnvelopeFields: (fields: {
104
104
  apiName: string;
@@ -15,16 +15,16 @@ export class LambderTransportFailure extends Error {
15
15
  /** Brand-based type guard, so a duplicate install of the package still matches. */
16
16
  export const isLambderTransportFailure = (err) => err instanceof Error && err.isLambderTransportFailure === true;
17
17
  /**
18
- * The fields of the request envelope, in the order they go on the wire: the
19
- * one statement of what a call sends, for every sender there is.
18
+ * The fields of the request envelope, in wire order: the one statement of
19
+ * what a call sends, for every sender.
20
20
  *
21
21
  * Two senders write it. A transport hands the payload over as a value
22
22
  * (buildTransportEnvelope, below); LambderInvokeCaller has already serialized
23
- * its payload to decide whether to compress it, and splices that JSON onto the
24
- * end rather than parsing and stringifying it a second time
25
- * (buildEnvelopeJson, in invoke/LambderLambdaEvent.ts). The splice sits on top
26
- * of this function precisely so that a new envelope field cannot be added to
27
- * one sender and missed by the other, which nothing on the wire would catch.
23
+ * its payload to decide on compression, and splices that JSON onto the end
24
+ * rather than parsing and stringifying it again (buildEnvelopeJson, in
25
+ * invoke/LambderLambdaEvent.ts). The splice builds on this function so a new
26
+ * envelope field cannot reach one sender and miss the other, which nothing on
27
+ * the wire would catch.
28
28
  */
29
29
  export const buildEnvelopeFields = (fields) => ({
30
30
  apiName: fields.apiName,
@@ -36,28 +36,23 @@ export declare const parseSetCookie: (header: string, now: number, requestPath?:
36
36
  * in-process handler transport in a Node test, and the mock runtime's direct
37
37
  * transport. It stores what an answer's Set-Cookie headers set, honours their
38
38
  * expiry and deletion, and hands back the Cookie pairs the next request should
39
- * carry. One jar is one browser; two jars are two.
39
+ * carry. One jar is one browser.
40
40
  *
41
- * The rules themselves are tough-cookie's, which is the reference
42
- * implementation of RFC 6265 and carries the public suffix list: domain and
43
- * path matching, default-path, Max-Age against Expires, Secure, HttpOnly, and
44
- * the __Host-/__Secure- prefixes. That list is the part worth importing rather
45
- * than writing. A hand-rolled check can tell that `Domain=com` is a registry
46
- * suffix by counting labels, and cannot tell that `co.uk` is one, so a
47
- * hand-rolled jar either trusts `Domain=co.uk` or bans every two-label domain.
41
+ * The rules (domain and path matching, default-path, Max-Age against Expires,
42
+ * Secure, HttpOnly, the __Host-/__Secure- prefixes) are tough-cookie's, the
43
+ * reference RFC 6265 implementation, imported for its public suffix list:
44
+ * counting labels can tell `Domain=com` is a registry suffix but not `co.uk`,
45
+ * so a hand-rolled jar either trusts `Domain=co.uk` or bans every two-label
46
+ * domain.
48
47
  *
49
- * What stays Lambder's is the shape of the questions a transport asks: whole
50
- * Set-Cookie header lists in (storeSetCookies), `name=value` pairs out
51
- * (cookiePairs), and a target given as a host and path rather than a URL,
52
- * since a transport that never speaks HTTP has no URL to give. A field the
53
- * caller omits is one it could not know, and an unknown field matches
54
- * anything: a jar pointed at a single host is the ordinary case, and refusing
55
- * to answer it until it can name that host would make the common setup the
56
- * awkward one.
48
+ * What stays Lambder's is the shape of a transport's questions: Set-Cookie
49
+ * header lists in (storeSetCookies), `name=value` pairs out (cookiePairs),
50
+ * and a target given as host and path, since a transport that never speaks
51
+ * HTTP has no URL. An omitted field matches anything, because a jar pointed
52
+ * at a single host is the ordinary case and should not have to name it.
57
53
  *
58
- * SameSite is stored but never consulted. It answers "did another site
59
- * initiate this", and a transport call has no initiating site: every call here
60
- * is same-site by construction.
54
+ * SameSite is stored but never consulted: it answers "did another site
55
+ * initiate this", and every transport call is same-site by construction.
61
56
  */
62
57
  export declare class LambderCookieJar {
63
58
  private readonly jar;
@@ -76,25 +71,24 @@ export declare class LambderCookieJar {
76
71
  * came from. Its `host` is the sending host, which every Domain is checked
77
72
  * against, and its `path` is the default Path of a cookie that names none.
78
73
  *
79
- * A Domain the sender is not under does not narrow a cookie, it voids it
80
- * (RFC 6265 section 5.3 step 6), and so does a Domain that is a public
81
- * suffix. Both are how evil.example.com would otherwise plant a cookie
82
- * that bank.example.com is handed on the next call.
74
+ * A Domain the sender is not under voids the cookie rather than narrowing
75
+ * it (RFC 6265 section 5.3 step 6), and so does a public-suffix Domain.
76
+ * Otherwise evil.example.com could plant a cookie that bank.example.com
77
+ * is handed on the next call.
83
78
  */
84
79
  storeSetCookies(headers: readonly string[], request?: LambderCookieTarget): void;
85
80
  /** Every live cookie. */
86
81
  list(): LambderStoredCookie[];
87
82
  /**
88
83
  * The Cookie header pairs the next request carries, as `name=value`, in
89
- * the order RFC 6265 section 5.4 puts them in: the longest Path first,
90
- * and among equal paths the one set first. Servers that read only the
91
- * first value of a repeated name depend on that order, and so does any
92
- * test reasoning about which of two same-named cookies wins.
84
+ * RFC 6265 section 5.4 order: longest Path first, and among equal paths
85
+ * the one set first. Servers that read only the first value of a repeated
86
+ * name depend on that order, as does any test about which of two
87
+ * same-named cookies wins.
93
88
  *
94
- * Only the cookies whose scope covers the target travel. A field the
95
- * target leaves out is one the caller could not know, and matches
96
- * anything: a caller that cannot name its own host still gets the cookies
97
- * of the one host its jar talks to.
89
+ * Only cookies whose scope covers the target travel. An omitted target
90
+ * field matches anything, so a caller that cannot name its host still
91
+ * gets the cookies of the one host its jar talks to.
98
92
  */
99
93
  cookiePairs(target?: LambderCookieTarget): string[];
100
94
  /**
@@ -107,10 +101,9 @@ export declare class LambderCookieJar {
107
101
  } & LambderCookieTarget): string | undefined;
108
102
  /**
109
103
  * The live cookies whose scope reaches this target, in RFC 6265 send
110
- * order. Delegated to tough-cookie whenever the target names a host,
111
- * which is the case worth getting exactly right; an unnamed host falls
112
- * back to every cookie the jar holds, filtered by the rules that do not
113
- * need one and ordered by the same rule.
104
+ * order. Delegated to tough-cookie whenever a host is known, the case
105
+ * worth getting exactly right; with no host, every cookie the jar holds
106
+ * is filtered by the rules that need none and ordered the same way.
114
107
  */
115
108
  private matchingCookies;
116
109
  /** Number of live cookies. */
@@ -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.