lambder 7.3.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/CHANGELOG.md +1047 -3
  2. package/README.md +46 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/ContractTypePrinter.d.ts +85 -0
  27. package/dist/build/ContractTypePrinter.js +402 -0
  28. package/dist/build/freshProcessVerifier.d.ts +13 -0
  29. package/dist/build/freshProcessVerifier.js +19 -0
  30. package/dist/build/moduleLocation.d.ts +11 -0
  31. package/dist/build/moduleLocation.js +6 -0
  32. package/dist/build/writeApiContract.d.ts +78 -0
  33. package/dist/build/writeApiContract.js +302 -0
  34. package/dist/build/writeApiSignatures.d.ts +114 -0
  35. package/dist/build/writeApiSignatures.js +217 -0
  36. package/dist/build/writeFileAtomically.d.ts +8 -0
  37. package/dist/build/writeFileAtomically.js +22 -0
  38. package/dist/build.d.ts +14 -0
  39. package/dist/build.js +11 -0
  40. package/dist/client/LambderCaller.d.ts +13 -44
  41. package/dist/client/LambderCaller.js +77 -84
  42. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  43. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  44. package/dist/client/LambderUploadRunner.d.ts +96 -0
  45. package/dist/client/LambderUploadRunner.js +234 -0
  46. package/dist/client/lambderFetchTransport.d.ts +4 -1
  47. package/dist/client/lambderFetchTransport.js +52 -28
  48. package/dist/client.d.ts +9 -3
  49. package/dist/client.js +6 -1
  50. package/dist/core/Lambder.d.ts +143 -79
  51. package/dist/core/Lambder.js +350 -231
  52. package/dist/core/LambderContext.d.ts +82 -15
  53. package/dist/core/LambderContext.js +107 -20
  54. package/dist/core/LambderCors.d.ts +21 -3
  55. package/dist/core/LambderCors.js +35 -16
  56. package/dist/core/LambderCrashHandling.d.ts +40 -0
  57. package/dist/core/LambderCrashHandling.js +97 -0
  58. package/dist/core/LambderCreateOptions.d.ts +151 -75
  59. package/dist/core/LambderCreateOptions.js +16 -23
  60. package/dist/core/LambderFiles.d.ts +21 -7
  61. package/dist/core/LambderFiles.js +62 -34
  62. package/dist/core/LambderIndexHtml.js +12 -11
  63. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  64. package/dist/core/LambderPolicyBuilders.js +17 -5
  65. package/dist/core/LambderPublicFiles.d.ts +11 -5
  66. package/dist/core/LambderPublicFiles.js +32 -4
  67. package/dist/core/LambderRequestPath.d.ts +43 -0
  68. package/dist/core/LambderRequestPath.js +63 -0
  69. package/dist/core/LambderResponse.d.ts +26 -5
  70. package/dist/core/LambderResponse.js +157 -70
  71. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  72. package/dist/core/LambderResponseBuilder.js +64 -3
  73. package/dist/core/LambderRouting.d.ts +2 -3
  74. package/dist/core/LambderRouting.js +22 -7
  75. package/dist/core/LambderTemplatingEngine.js +211 -32
  76. package/dist/index.d.ts +25 -8
  77. package/dist/index.js +13 -4
  78. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  79. package/dist/invoke/LambderInvokeCaller.js +76 -66
  80. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  81. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  82. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  83. package/dist/invoke/LambderLambdaEvent.js +40 -22
  84. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  85. package/dist/invoke/lambderHandlerTransport.js +15 -18
  86. package/dist/mock/LambderMockApp.d.ts +67 -83
  87. package/dist/mock/LambderMockApp.js +167 -153
  88. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  89. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  90. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  91. package/dist/mock/LambderMockCallRecorder.js +19 -28
  92. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  93. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  94. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  95. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  96. package/dist/mock/LambderMockFailureInjector.js +3 -6
  97. package/dist/mock/LambderMockTypes.d.ts +78 -108
  98. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  99. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  100. package/dist/mock/lambderMockMswHandler.d.ts +43 -33
  101. package/dist/mock/lambderMockMswHandler.js +50 -39
  102. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  103. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  104. package/dist/mock.d.ts +4 -1
  105. package/dist/mock.js +6 -3
  106. package/dist/session/LambderSessionController.d.ts +108 -89
  107. package/dist/session/LambderSessionController.js +187 -168
  108. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  109. package/dist/session/LambderSessionCrypto.js +26 -12
  110. package/dist/session/LambderSessionManager.d.ts +124 -46
  111. package/dist/session/LambderSessionManager.js +262 -137
  112. package/dist/shared/LambderHtml.d.ts +42 -3
  113. package/dist/shared/LambderHtml.js +127 -7
  114. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  115. package/dist/shared/LambderHtmlPositions.js +652 -0
  116. package/dist/shared/LambderI18n.d.ts +10 -11
  117. package/dist/shared/LambderI18n.js +33 -21
  118. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  119. package/dist/shared/contracts/LambderCache.js +11 -0
  120. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  121. package/dist/shared/contracts/LambderFileSource.js +5 -8
  122. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  123. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  124. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  125. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  126. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  127. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  128. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  129. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  130. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  131. package/dist/shared/transport/LambderApiTransport.js +7 -7
  132. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  133. package/dist/shared/transport/LambderCookieJar.js +54 -66
  134. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  135. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  136. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  137. package/dist/shared/util/LambderCallAbort.js +5 -5
  138. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  139. package/dist/shared/util/LambderClientIp.js +96 -13
  140. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  141. package/dist/shared/util/LambderContentDisposition.js +13 -0
  142. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  143. package/dist/shared/util/LambderExpiringMap.js +41 -57
  144. package/dist/shared/util/LambderNodeModules.js +6 -7
  145. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  146. package/dist/shared/util/LambderOptionChecks.js +4 -4
  147. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  148. package/dist/shared/util/LambderResponseBrand.js +5 -5
  149. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  150. package/dist/shared/util/LambderTextDigest.js +11 -5
  151. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  152. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  153. package/dist/shared/util/boundKeyField.d.ts +20 -0
  154. package/dist/shared/util/boundKeyField.js +34 -0
  155. package/dist/shared/util/canonicalJson.d.ts +11 -0
  156. package/dist/shared/util/canonicalJson.js +28 -0
  157. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  158. package/dist/shared/util/joinKeyFields.js +22 -0
  159. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  160. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  161. package/dist/shared/wire/LambderApiContract.d.ts +98 -53
  162. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  163. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  164. package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
  165. package/dist/shared/wire/LambderApiRefusal.js +42 -7
  166. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  167. package/dist/shared/wire/LambderApiSignature.js +16 -19
  168. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  169. package/dist/shared/wire/LambderCallOptions.js +9 -11
  170. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  171. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  172. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  173. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  174. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  175. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  176. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  177. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  178. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  179. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  180. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  181. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  182. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  183. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  184. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  185. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  186. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  187. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  188. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  189. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  190. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  191. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  192. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  193. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  194. package/dist/stores/LambderCacheFiller.js +119 -0
  195. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  196. package/dist/stores/LambderCacheKeys.js +54 -0
  197. package/dist/stores/LambderCacheValues.d.ts +45 -0
  198. package/dist/stores/LambderCacheValues.js +74 -0
  199. package/dist/stores/LambderDdbCache.d.ts +121 -56
  200. package/dist/stores/LambderDdbCache.js +528 -225
  201. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  202. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  203. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  204. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  205. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  206. package/dist/stores/LambderDdbSdk.js +80 -38
  207. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  208. package/dist/stores/LambderDdbSessionStore.js +119 -47
  209. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  210. package/dist/stores/LambderHttpFileSource.js +15 -13
  211. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  212. package/dist/stores/LambderMemoryCache.js +113 -0
  213. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  214. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  215. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  216. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  217. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  218. package/dist/stores/LambderMemorySessionStore.js +38 -19
  219. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  220. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  221. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  222. package/dist/stores/LambderS3FileSource.js +12 -7
  223. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  224. package/dist/stores/LambderS3UploadBucket.js +144 -0
  225. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  226. package/dist/stores/LambderSdkInstallHint.js +14 -0
  227. package/dist/testing/LambderTestApp.d.ts +23 -25
  228. package/dist/testing/LambderTestApp.js +22 -24
  229. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  230. package/dist/testing/LambderTestVisitor.js +15 -15
  231. package/dist/testing.d.ts +3 -0
  232. package/dist/testing.js +2 -0
  233. package/package.json +26 -3
  234. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  235. package/dist/api/LambderApiPolicyEngine.js +0 -85
  236. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  237. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -1,6 +1,6 @@
1
1
  import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
2
2
  import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
3
- import { createApiCallContext } from "../api/LambderApiCallContext.js";
3
+ import { bindCallTools, createApiCallContext } from "../api/LambderApiCallContext.js";
4
4
  import { toHttpAnswer } from "../api/LambderApiAnswer.js";
5
5
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
6
6
  import { buildApiEnvelope, envelopeAnswer, crashAnswer, } from "../api/LambderApiEnvelope.js";
@@ -37,13 +37,13 @@ const DEFAULT_SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
37
37
  const defaultCookieHost = () => globalThis.location?.host || "localhost";
38
38
  /**
39
39
  * The mock runtime: the API core (LambderApiPipeline, the same class the
40
- * Lambda server runs) over memory stores, with a registry of typed mock
41
- * handlers where the server has app handlers, and mock guards where it has
42
- * app guards. Everything the protocol does (envelope, refusals, sessions
43
- * and their cookies, guards, rate limits, idempotency, the signature gate)
44
- * happens in the core; this class only resolves a name to an entry, wraps
45
- * the handler's return into the envelope, and adds what a mock needs on
46
- * top: failure injection, latency, a subscription, a call log, reset.
40
+ * Lambda server runs) over memory stores, with typed mock handlers and mock
41
+ * guards where the server has app handlers and guards. Every protocol step
42
+ * (envelope, refusals, sessions and their cookies, guards, rate limits,
43
+ * idempotency, the signature gate) happens in the core; this class resolves
44
+ * a name to an entry, wraps the handler's return into the envelope, and adds
45
+ * what a mock needs: failure injection, latency, a subscription, a call log,
46
+ * reset.
47
47
  *
48
48
  * Create one with initLambderMock<Contract, SessionData>().create(...),
49
49
  * which fixes the contract and session types first so everything else is
@@ -58,12 +58,11 @@ export class LambderMockApp {
58
58
  tokenCookieKey;
59
59
  csrfCookieKey;
60
60
  /**
61
- * The client IP a transport request carrying none is read as. Public
62
- * because an adapter has to read the same default the direct transport
63
- * uses: the MSW adapter had a hardcoded "127.0.0.1" of its own, so an app
64
- * that set defaultClientIp saw one address through the transport and
65
- * another through the service worker, and a per-IP rate limit counted two
66
- * clients where there was one.
61
+ * The client IP a transport request carrying none is read as. Public so
62
+ * every adapter reads the same default the direct transport uses: with a
63
+ * default of its own, an app that set defaultClientIp would show one
64
+ * address through the transport and another through the service worker,
65
+ * and a per-IP rate limit would count two clients where there is one.
67
66
  */
68
67
  defaultClientIp;
69
68
  /** The host this runtime's cookies belong to (see the cookieHost option). */
@@ -71,9 +70,8 @@ export class LambderMockApp {
71
70
  /**
72
71
  * The API core, every protocol step of it. Private: the mock's surface is
73
72
  * the app, and a consumer reaching past it would be configuring the
74
- * server's pipeline through a development tool. The three adapters take
75
- * what they need from the app's own methods (handleRequest,
76
- * requestFromTransport), which is why none of them names this.
73
+ * server's pipeline through a development tool. The adapters use the
74
+ * app's own methods (handleRequest, requestFromTransport) instead.
77
75
  */
78
76
  pipeline;
79
77
  /** The cookie scope signIn plants under, so signOut can name the same one when it clears them. */
@@ -93,12 +91,11 @@ export class LambderMockApp {
93
91
  this.apiVersion = options.apiVersion ?? null;
94
92
  this.failures = new LambderMockFailureInjector({ apiVersion: this.apiVersion, latency: options.latency ?? 0 });
95
93
  this.recorder = new LambderMockCallRecorder({ callLogSize: options.callLogSize ?? DEFAULT_CALL_LOG_SIZE });
96
- // The loopback address when nothing names a client, as a request from
97
- // the page itself, and the same constant the in-process handler
98
- // transport defaults to. Normalized here rather than at every reader:
99
- // one textual form per address is what makes a per-IP rate limit's
100
- // counter one counter, and this value is read by the direct transport,
101
- // by the MSW adapter and by every request that names no client.
94
+ // The loopback address when nothing names a client, as for a request
95
+ // from the page itself (the in-process handler transport's default
96
+ // too). Normalized once here because the direct transport, the MSW
97
+ // adapter and every request naming no client all read it, and one
98
+ // textual form per address keeps a per-IP limit to one counter.
102
99
  this.defaultClientIp = normalizeClientIp(options.defaultClientIp ?? LOOPBACK_CLIENT_IP);
103
100
  this.cookieHost = options.cookieHost ?? defaultCookieHost();
104
101
  this.onReset = options.onReset ?? null;
@@ -121,6 +118,19 @@ export class LambderMockApp {
121
118
  minApiVersion: options.minApiVersion,
122
119
  apiSignatures: options.apiSignatures,
123
120
  maxRequestPayloadBytes: options.maxRequestPayloadBytes,
121
+ // The server app's own answer to a bad input, where it has one:
122
+ // without it the mock would answer 422 where the server answers,
123
+ // say, 200 with an errorMessage, and a form's error handling would
124
+ // be tested against an answer production never gives.
125
+ onInvalidInput: options.onInvalidInput
126
+ ? async (zodError, ctx) => {
127
+ const answer = await options.onInvalidInput(zodError, ctx);
128
+ if (!answer)
129
+ return null;
130
+ const config = answer.config ?? {};
131
+ return envelopeAnswer(buildApiEnvelope(this.apiVersion, answer.payload ?? null, { ...config, logList: config.logList ?? ctx.logList }), { statusCode: answer.statusCode ?? 200 });
132
+ }
133
+ : undefined,
124
134
  sessions: sessionOptions
125
135
  ? {
126
136
  manager: new LambderSessionManager({
@@ -133,12 +143,11 @@ export class LambderMockApp {
133
143
  // crypto.subtle, and a memory-only store is nothing
134
144
  // anyone can leak, so hashing there protects nothing.
135
145
  // Keyed on the store's own isMemoryOnly rather than on
136
- // "did this runtime create it": an app that passes its
137
- // own LambderMemorySessionStore got WebCrypto and threw
138
- // on the first session call where the default path
139
- // degrades, and a store that outlives the process still
140
- // gets real hashing, which is what the declaration is
141
- // there to say.
146
+ // whether this runtime created it: an app passing its
147
+ // own LambderMemorySessionStore degrades the same way
148
+ // instead of throwing on the first session call, and a
149
+ // store that outlives the process still gets real
150
+ // hashing.
142
151
  crypto: sessionOptions.crypto
143
152
  ?? (!isWebCryptoAvailable() && (sessionOptions.store?.isMemoryOnly ?? true) ? new LambderPlainSessionCrypto() : undefined),
144
153
  }),
@@ -157,13 +166,12 @@ export class LambderMockApp {
157
166
  defaultTtlSeconds: idempotencyOptions.defaultTtlSeconds,
158
167
  defaultPendingTtlSeconds: idempotencyOptions.defaultPendingTtlSeconds,
159
168
  failOpen: idempotencyOptions.failOpen,
160
- // The engine's own option is typed for the base call
161
- // context, being the one thing every adapter shares; what
162
- // it actually hands the function is the context of the
163
- // adapter running it, which here is the mock's. So the
164
- // option is declared for the mock context, where a reader
165
- // can use ctx.request, and widened at the one place that
166
- // knows both sides.
169
+ // The engine types this option for the base call context,
170
+ // the one thing every adapter shares, but hands the
171
+ // function the running adapter's context, here the mock's.
172
+ // So it is declared for the mock context (ctx.request is
173
+ // readable) and widened here, the one place that knows
174
+ // both sides.
167
175
  callerIdentity: idempotencyOptions.callerIdentity,
168
176
  }
169
177
  : undefined,
@@ -177,11 +185,10 @@ export class LambderMockApp {
177
185
  * the ones the server runs on a definition, and the mock's own "a session
178
186
  * endpoint needs the sessions option".
179
187
  *
180
- * One place, so no registration path can skip either check. A session
181
- * endpoint on a mock without sessions would otherwise register silently
182
- * and answer the first call with a 500 from inside the pipeline, naming
183
- * the SERVER's option name, for a mistake whose fix is one option at
184
- * create().
188
+ * One place, so no registration path can skip either check. Without the
189
+ * second, a session endpoint on a mock without sessions would register
190
+ * silently and answer its first call with a 500 naming the SERVER's
191
+ * option, for a mistake fixed by one option at create().
185
192
  */
186
193
  assertEntryRegistration(definition) {
187
194
  this.pipeline.assertRegistration(definition);
@@ -219,14 +226,13 @@ export class LambderMockApp {
219
226
  * A public endpoint deliberately left without a mock; a call answers the
220
227
  * notMocked refusal carrying the reason.
221
228
  *
222
- * Public and session have separate builders for the same reason publicApi
223
- * and sessionApi do: the refusal runs through the pipeline so that the
224
- * steps BEFORE dispatch still happen, and the session read is one of them.
225
- * Declaring every not-mocked endpoint public switched that step off, so a
226
- * session endpoint with no session answered "not mocked" where the server
227
- * answers sessionExpired, and the mode on its events and call log was
228
- * wrong too. The mode cannot be recovered at runtime, because the contract
229
- * is a type, so the builder is where it has to be said.
229
+ * Public and session get separate builders, as publicApi and sessionApi
230
+ * do, because the refusal runs through the pipeline so the steps BEFORE
231
+ * dispatch still happen, and the session read is one of them. Were every
232
+ * not-mocked endpoint public, a session endpoint with no session would
233
+ * answer "not mocked" where the server answers sessionExpired, and its
234
+ * events and call log would carry the wrong mode. The contract is a type,
235
+ * so the mode cannot be recovered at runtime: the builder has to say it.
230
236
  */
231
237
  notMocked(name, reason) {
232
238
  return this.buildNotMockedEntry(name, "public", reason);
@@ -243,23 +249,21 @@ export class LambderMockApp {
243
249
  * mockApp.register(userMocks, billingMocks, mockApp.restNotMocked("not mocked yet"));
244
250
  * ```
245
251
  *
246
- * What it buys is adoption over a contract the mocks do not cover yet:
247
- * register() stays exhaustive by construction, and the endpoints nothing
248
- * claims answer the notMocked refusal carrying this reason instead of
252
+ * It lets mocks be adopted over a contract they do not cover yet:
253
+ * register() stays exhaustive by construction, and endpoints nothing
254
+ * claims answer the notMocked refusal with this reason instead of
249
255
  * apiNotFound, so a screen that reaches one says "not mocked yet" rather
250
256
  * than "unknown error". Strays and duplicates in the explicit slices are
251
- * refused exactly as they are without it, and an entry registered later
252
- * (registerPartial, or a second register) takes the endpoint back from the
253
- * rest.
257
+ * still refused, and an entry registered later (registerPartial, or a
258
+ * second register) takes its endpoint back from the rest.
254
259
  *
255
- * The one thing it cannot do is the session read. A call it answers is
256
- * processed as a public endpoint: the protocol's pre-pass still runs, so a
257
- * stale client still hears versionExpired, but the mode of a name nothing
258
- * registered is not knowable at runtime, the contract being a type. So a
259
- * signed-out call to an unmocked session endpoint is answered "not mocked"
260
- * where the server answers sessionExpired, and the endpoint whose
261
- * signed-out path a test cares about is the one to declare with
262
- * sessionNotMocked instead.
260
+ * What it cannot do is the session read. The mode of an unregistered name
261
+ * is not knowable at runtime (the contract is a type), so a call it
262
+ * answers is processed as public: the protocol's pre-pass still runs, so
263
+ * a stale client still hears versionExpired, but a signed-out call to an
264
+ * unmocked session endpoint answers "not mocked" where the server answers
265
+ * sessionExpired. Declare an endpoint whose signed-out path a test cares
266
+ * about with sessionNotMocked instead.
263
267
  */
264
268
  restNotMocked(reason) {
265
269
  return { restNotMockedReason: reason };
@@ -334,11 +338,10 @@ export class LambderMockApp {
334
338
  * Whether a call to this name would be answered from the registry, which
335
339
  * is what an adapter asks before passing one on.
336
340
  *
337
- * True for every name once a rest entry is registered, because the rest
338
- * entry is what answers the names nothing else claimed. That is what makes
339
- * a rest entry and the MSW adapter's `onUnmocked: "passthrough"`
340
- * alternatives rather than layers: with one registered, the runtime
341
- * answers everything itself and nothing is handed on to the network.
341
+ * True for every name once a rest entry is registered, since it answers
342
+ * whatever nothing else claimed. That makes a rest entry and the MSW
343
+ * adapter's `onUnmocked: "passthrough"` alternatives rather than layers:
344
+ * with one registered, nothing is handed on to the network.
342
345
  */
343
346
  hasRegisteredEntry(apiName) {
344
347
  return this.entryFor(apiName) !== null || this.registry.restNotMockedReason !== null;
@@ -349,13 +352,10 @@ export class LambderMockApp {
349
352
  /**
350
353
  * The entry that answers a name nothing registered, when register() was
351
354
  * given a rest entry: the notMocked refusal carrying its reason, run
352
- * through the pipeline as a public endpoint.
353
- *
354
- * Public because the mode of an unregistered name cannot be recovered at
355
- * runtime, the contract being a type. Everything that precedes dispatch
356
- * still runs (the signature gate, the payload restore); the session read is
357
- * the one step this answer cannot have, which is the fidelity limit
358
- * restNotMocked documents.
355
+ * through the pipeline as a public endpoint, since the mode of an
356
+ * unregistered name cannot be recovered at runtime. Everything before
357
+ * dispatch still runs (the signature gate, the payload restore); the
358
+ * missing session read is the fidelity limit restNotMocked documents.
359
359
  */
360
360
  restNotMockedEntry(apiName) {
361
361
  const reason = this.registry.restNotMockedReason;
@@ -387,20 +387,17 @@ export class LambderMockApp {
387
387
  * latency, the call log and its numbering, the cookies its own transports
388
388
  * hold, then onReset, so the app rewinds its own data too.
389
389
  *
390
- * The cookies matter as much as the sessions do: emptying the session
391
- * store while a jar still holds the token for one of them leaves the next
392
- * call carrying a session that no longer exists, which reads as signed in
393
- * until the answer says sessionExpired. So every jar transport() built
394
- * for itself is emptied, and the cookies a "document" transport mirrored
395
- * are expired again.
390
+ * The cookies matter as much as the sessions: a jar still holding the
391
+ * token of an emptied store's session reads as signed in until an answer
392
+ * says sessionExpired. So every jar transport() built for itself is
393
+ * emptied, and the cookies a "document" transport mirrored are expired.
396
394
  *
397
- * The registry survives, being what the runtime was configured with
398
- * rather than what it accumulated. Subscriptions survive too, because
399
- * they are how a test watches the runtime rather than state it is
400
- * testing; a listener muted for throwing is unmuted, so one bad call does
401
- * not silence it for the rest of the run. A session store or a cookie jar
402
- * the app supplied itself survives: the runtime did not create it and
403
- * does not know what else holds it.
395
+ * The registry survives, being configuration rather than accumulated
396
+ * state. Subscriptions survive too, being how a test watches the runtime;
397
+ * a listener muted for throwing is unmuted, so one bad call does not
398
+ * silence it for the rest of the run. A session store or cookie jar the
399
+ * app supplied survives: the runtime did not create it and does not know
400
+ * what else holds it.
404
401
  */
405
402
  reset() {
406
403
  this.sessionStore?.reset();
@@ -417,12 +414,9 @@ export class LambderMockApp {
417
414
  // -----------------------------------------------------------------------
418
415
  /**
419
416
  * The four session members refuse in the mock's own words, naming the
420
- * option a mock is created with.
421
- *
422
- * The pipeline's guard says "Configure the session option at creation",
423
- * which is the SERVER's option name: the mock's is `sessions`, and a
424
- * reader who goes looking for `session` on create() does not find it.
425
- * The registration path was fixed for exactly this one method over.
417
+ * option a mock is created with. The pipeline's guard names the SERVER's
418
+ * option (`session`), which a reader does not find on the mock's create()
419
+ * (`sessions`); assertEntryRegistration does the same for registration.
426
420
  */
427
421
  assertSessionsConfigured(member) {
428
422
  if (!this.pipeline.hasSessions)
@@ -453,17 +447,16 @@ export class LambderMockApp {
453
447
  const headers = {};
454
448
  ctx.responseHeaders.applyInto(headers);
455
449
  const setCookies = getAnswerHeader(headers, "Set-Cookie") ?? [];
456
- // The host these cookies came from, which is the same one the
457
- // controller wrote them for. A jar checks every Domain against the
458
- // sending host and refuses one it cannot check, so planting them
459
- // unscoped dropped the session cookie of any app that configures a
460
- // cookie domain, silently.
450
+ // Planted for the host the controller wrote them for: a jar checks
451
+ // every Domain against the sending host and refuses one it cannot
452
+ // check, so unscoped, the session cookie of an app that configures a
453
+ // cookie domain would be dropped silently.
461
454
  options.jar?.storeSetCookies(setCookies, { host });
462
455
  // Through the same mirror every other cookie writer uses. Behind the
463
- // MSW adapter the jar is not where a page reads its CSRF token: the
464
- // browser caller reads document.cookie and posts what it finds, so a
465
- // signIn that only filled a jar left the token empty and every one of
466
- // the session endpoints answered sessionExpired.
456
+ // MSW adapter the page's caller reads its CSRF token from
457
+ // document.cookie, not the jar, so a signIn that only filled a jar
458
+ // would leave the token empty and every session call would answer
459
+ // sessionExpired.
467
460
  this.browserCookies.mirrorSetCookies(setCookies);
468
461
  return created;
469
462
  }
@@ -532,16 +525,27 @@ export class LambderMockApp {
532
525
  // idempotency, and on one that does not it is a value nothing
533
526
  // reads, so handing it over typed as a string would be the lie.
534
527
  idempotencyKey: typeof request.idempotencyKey === "string" ? request.idempotencyKey : undefined,
535
- sessions: undefined,
536
528
  });
537
- // The controller reads and writes this very context, so it is built
538
- // after it. Without sessions configured, touching it says why.
539
- Object.defineProperty(ctx, "sessions", {
540
- enumerable: true,
541
- get() {
542
- if (!pipeline.hasSessions)
543
- throw new Error(`LambderMockApp: ctx.sessions on "${request.apiName}" needs the sessions option at creation.`);
544
- return pipeline.sessionController(ctx, LambderApiPipeline.sessionInfoOf(request));
529
+ // The controller reads and writes this very context, so it is bound
530
+ // after it, the way the server binds its own. Without sessions
531
+ // configured, touching it says why.
532
+ const chargeRateLimit = async (policy, key, refuse) => {
533
+ const { checkResult, refusal } = await pipeline.chargeRateLimit(policy, { apiName: request.apiName, ip: request.ip, session: ctx.session, key });
534
+ if (refuse && refusal)
535
+ throw refusal;
536
+ return checkResult;
537
+ };
538
+ bindCallTools(ctx, {
539
+ getters: {
540
+ sessionController: () => {
541
+ if (!pipeline.hasSessions)
542
+ throw new Error(`LambderMockApp: ctx.sessionController on "${request.apiName}" needs the sessions option at creation.`);
543
+ return pipeline.sessionController(ctx, LambderApiPipeline.sessionInfoOf(request));
544
+ },
545
+ },
546
+ methods: {
547
+ rateLimit: async (policy, key) => { await chargeRateLimit(policy, key, true); },
548
+ isRateLimited: (policy, key) => chargeRateLimit(policy, key, false),
545
549
  },
546
550
  });
547
551
  return ctx;
@@ -595,24 +599,23 @@ export class LambderMockApp {
595
599
  let outcome;
596
600
  let error;
597
601
  try {
598
- // The protocol's pre-pass, run before the name is resolved, which
599
- // is where the server runs it. Two things depended on it: an
600
- // unknown name reached the notFound refusal without the signature
601
- // gate or the payload restore, so a stale client or a malformed
602
- // compressed payload was answered differently here than on the
603
- // server; and the request event carried the wire fields instead of
604
- // the payload, so a dev panel watching calls in flight showed
605
- // nothing for exactly the compressed calls someone opens a panel
606
- // for. run() calls prepare again, which is safe by construction.
602
+ // The protocol's pre-pass, run before the name is resolved, as on
603
+ // the server. Otherwise an unknown name would reach the notFound
604
+ // refusal without the signature gate or the payload restore (a
605
+ // stale client or a malformed compressed payload answered unlike
606
+ // the server), and the request event would carry the wire fields
607
+ // instead of the payload, so a dev panel would show nothing for
608
+ // exactly the compressed calls. run() calls prepare again, which
609
+ // is safe by construction.
607
610
  const prepared = await this.pipeline.prepare(request);
608
611
  this.emit(this.requestEvent(id, request, mode, startedAt));
609
612
  await this.failures.wait(this.failures.latencyFor(request.apiName), request.signal);
610
613
  if (this.failures.offline)
611
614
  throw new LambderMockTransportError("offline");
612
- // After the wait and the offline check, both of which end the call
613
- // before it reaches a handler: taking the failure first spent a
614
- // queued failNext on a call that never got to be failed by it, and
615
- // the next call, the one the test was arranging for, then answered
615
+ // Taken after the wait and the offline check, both of which end
616
+ // the call before it reaches a handler: taken first, a queued
617
+ // failNext would be spent on a call it never got to fail, and the
618
+ // next call, the one the test arranged it for, would answer
616
619
  // normally.
617
620
  const failure = this.failures.take(request.apiName);
618
621
  if (failure) {
@@ -623,7 +626,7 @@ export class LambderMockApp {
623
626
  answer = prepared;
624
627
  }
625
628
  else if (!entry) {
626
- answer = this.pipeline.answerUnknownApi(request, ctx);
629
+ answer = this.pipeline.answerUnknownApi(ctx);
627
630
  outcome = "unknownApi";
628
631
  }
629
632
  else {
@@ -639,11 +642,10 @@ export class LambderMockApp {
639
642
  }));
640
643
  }
641
644
  // A notMocked entry refuses where a handler would run, not
642
- // ahead of the pipeline: answering it directly skipped the
643
- // steps that precede dispatch, so a stale client heard
644
- // "not mocked" from a mock the server would have answered
645
- // versionExpired to, and a compressed payload never reached
646
- // the events at all.
645
+ // ahead of the pipeline, so the steps that precede
646
+ // dispatch still happen: a stale client hears
647
+ // versionExpired as it would from the server, and a
648
+ // compressed payload reaches the events.
647
649
  : async () => {
648
650
  throw new LambderApiRefusal(`Not mocked: ${entry.notMockedReason}`, {
649
651
  errorMessage: { type: "warning", code: LAMBDER_REFUSAL_CODES.notMocked, content: `"${request.apiName}" is not mocked: ${entry.notMockedReason}` },
@@ -670,14 +672,12 @@ export class LambderMockApp {
670
672
  : crashAnswer(this.apiVersion);
671
673
  outcome = "crash";
672
674
  }
673
- // Every header written during the call belongs on the answer, whichever
674
- // way the call ended. The pipeline does this for the answers it
675
- // produces itself, but a crash unwinds past it and an injected failure
676
- // never reaches it, and those are the two that hurt most in
677
- // development: a handler that created a session and then threw would
678
- // otherwise leave the jar with no cookie and nothing to explain it.
679
- // applyInto is idempotent, so the answers the pipeline already handled
680
- // are unaffected.
675
+ // Every header written during the call belongs on the answer, however
676
+ // the call ended. The pipeline applies them to the answers it produces,
677
+ // but a crash unwinds past it and an injected failure never reaches
678
+ // it: a handler that created a session and then threw would otherwise
679
+ // leave the jar with no cookie and nothing to explain it. applyInto is
680
+ // idempotent, so answers the pipeline already handled are unaffected.
681
681
  ctx.responseHeaders.applyInto(answer.headers);
682
682
  this.recorder.settle(facts, { answer, guardsRun: trace.guardsRun, ...(outcome ? { outcome } : {}), ...(error ? { error } : {}) });
683
683
  return answer;
@@ -699,7 +699,14 @@ export class LambderMockApp {
699
699
  host: transportRequest.siteHost || this.cookieHost,
700
700
  ...(transportRequest.signal ? { signal: transportRequest.signal } : {}),
701
701
  };
702
- const request = readApiEnvelope(buildTransportEnvelope(transportRequest), info);
702
+ // Through the wire's own serialization, as every server-bound
703
+ // transport sends it: the handler gets a parse of the JSON, never the
704
+ // page's object. Handed over by reference, a handler that stores the
705
+ // payload would share it with the page's form (later edits changing
706
+ // the "saved" record with no call), a Date would stay a Date and a
707
+ // key set to undefined keep its place, none of which a server sees.
708
+ const envelope = JSON.parse(JSON.stringify(buildTransportEnvelope(transportRequest)));
709
+ const request = readApiEnvelope(envelope, info);
703
710
  if (!request)
704
711
  throw new Error("LambderMockApp: the transport request names no api.");
705
712
  return request;
@@ -732,12 +739,19 @@ export class LambderMockApp {
732
739
  if (jar !== cookies)
733
740
  this.browserCookies.adoptJar(jar);
734
741
  // The runtime's one cookie host, so the jar scopes what it sends the
735
- // way the browser this transport stands in for would. Without it the
736
- // scope was whatever host the caller happened to name, which is the
737
- // page's, and cookies signIn planted went unsent.
742
+ // way the browser this transport stands in for would; scoped to
743
+ // whatever host a caller names, cookies signIn planted would go
744
+ // unsent.
738
745
  const withJar = lambderCookieJarTransport(direct, { jar, csrfCookieKey: this.csrfCookieKey, host: this.cookieHost });
746
+ // Where the page's readable cookies live, one answer per mode. In
747
+ // memory mode it is this jar alone, so the token the caller read is
748
+ // dropped and the jar fills in its own CSRF cookie. A page's caller
749
+ // reading document.cookie would find what signIn mirrors for the MSW
750
+ // adapter's sake and no later answer of this transport writes, so
751
+ // after a logout and a login it would post the first session's token
752
+ // and be signed out.
739
753
  if (cookies !== "document")
740
- return transportCarrying(withJar, jar);
754
+ return transportCarrying(async (request) => await withJar({ ...request, token: "" }), jar);
741
755
  // The page's own cookie storage sees the non-HttpOnly cookies (the
742
756
  // CSRF token), so the caller's cookie read and clear paths run for
743
757
  // real; the HttpOnly session cookie stays in the jar, as a browser
@@ -760,9 +774,9 @@ export class LambderMockApp {
760
774
  /**
761
775
  * Takes a jar an adapter built for itself as the runtime's own, so reset()
762
776
  * empties it with the rest. The MSW adapter's jar holds the session
763
- * cookies of calls that never touch transport(), and a reset that leaves
764
- * it full is the same stale-session bug: the store is empty and the next
765
- * request still carries a token for one of its sessions.
777
+ * cookies of calls that never touch transport(); left full after a reset,
778
+ * the next request would carry a token for a session the emptied store no
779
+ * longer has.
766
780
  */
767
781
  adoptCookieJar(jar) {
768
782
  this.browserCookies.adoptJar(jar);
@@ -804,12 +818,12 @@ export const initLambderMock = () => ({
804
818
  * The mock app, with the guard map and the rate-limit policies inferred
805
819
  * from the options.
806
820
  *
807
- * `const` on each of them is what pins a restatement to the contract, and
808
- * it has one cost: inferring a generic from an object literal switches
809
- * excess-property checking off for the whole literal, nested objects
810
- * included, so a typo inside `rateLimits.policies` or `idempotency`
811
- * compiled and was dropped in silence. `I` exists for the same reason `P`
812
- * does, and LambderMockSurplusKeys puts the error back on the key.
821
+ * `const` on each of them pins a restatement to the contract, at a cost:
822
+ * inferring a generic from an object literal switches excess-property
823
+ * checking off for the whole literal, nested objects included, so a typo
824
+ * inside `rateLimits.policies` or `idempotency` would compile and be
825
+ * dropped. `I` exists for the same reason `P` does, and
826
+ * LambderMockSurplusKeys puts the error back on the key.
813
827
  */
814
828
  create(options) {
815
829
  return new LambderMockApp(options);
@@ -1,21 +1,17 @@
1
1
  import { type LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
2
2
  /**
3
3
  * Where the runtime's cookies live outside its own answers: the jars it built
4
- * for itself, and the copies it planted in the page's own cookie storage.
4
+ * for itself, and the copies it planted in the page's cookie storage. A
5
+ * collaborator of LambderMockApp that owns state nothing else touches, meets
6
+ * the runtime at two calls (an answer coming back, reset()), and holds the
7
+ * one outside dependency, `document`, keeping the app free of browser
8
+ * conditionals.
5
9
  *
6
- * The fourth of LambderMockApp's collaborators, and the same argument as the
7
- * other three: it owns state nothing else touches and meets the runtime at two
8
- * calls (a transport's answer coming back, and reset()). It is the piece with
9
- * an outside dependency, `document`, so keeping it here is also what keeps the
10
- * app free of browser conditionals.
11
- *
12
- * Both the direct transport's "document" mode and the MSW adapter come through
13
- * one instance, so there is one mirror implementation and one record of what
14
- * was planted. They carried a copy each before: the MSW copy dropped `Secure`
15
- * on a page that is not a secure context and the transport's did not, so on
16
- * plain http (device testing on a LAN address) the browser silently refused
17
- * the CSRF cookie and every session call failed its CSRF check with nothing in
18
- * any log to say why.
10
+ * The direct transport's "document" mode and the MSW adapter share one
11
+ * instance, so they cannot disagree on dropping `Secure`: on plain http
12
+ * (device testing on a LAN address) a kept `Secure` makes the browser
13
+ * silently refuse the CSRF cookie, failing every session call's CSRF check
14
+ * with nothing in any log.
19
15
  */
20
16
  export declare class LambderMockBrowserCookies {
21
17
  /** The jars transport() and the adapters built for themselves, which reset() is therefore free to empty. */
@@ -24,32 +20,32 @@ export declare class LambderMockBrowserCookies {
24
20
  private readonly mirroredCookies;
25
21
  /**
26
22
  * Takes a jar built for the runtime as the runtime's own, so reset()
27
- * empties it with the rest. A jar the app passed in stays the app's, the
28
- * way an app-supplied session store does: the runtime did not create it and
29
- * does not know what else holds it.
23
+ * empties it with the rest. A jar the app passed in stays the app's, like
24
+ * an app-supplied session store: the runtime does not know what else
25
+ * holds it.
30
26
  */
31
27
  adoptJar(jar: LambderCookieJar): void;
32
28
  /**
33
29
  * Mirrors an answer's non-HttpOnly cookies into document.cookie and
34
30
  * remembers them for reset().
35
31
  *
36
- * HttpOnly cookies are skipped exactly as a real browser skips them, the
37
- * jar being the store no script can reach. `Secure` is dropped where the
38
- * page is not a secure context, because the browser would refuse the write
39
- * and development over plain http on a LAN address has to keep working. A
40
- * `__Host-` or `__Secure-` cookie name is then discarded by the browser
41
- * for breaking its own prefix rule, which is correct: such a name cannot
42
- * work on plain http at all, and localhost is a secure context.
32
+ * HttpOnly cookies are skipped as a real browser skips them; the jar is
33
+ * the store no script can reach. `Secure` is dropped where the page is not
34
+ * a secure context, since the browser would refuse the write and plain
35
+ * http on a LAN address has to keep working. The browser then discards a
36
+ * `__Host-` or `__Secure-` name for breaking its prefix rule, which is
37
+ * correct: such a name cannot work on plain http, and localhost is a
38
+ * secure context.
43
39
  */
44
40
  mirrorSetCookies(setCookies: readonly string[]): void;
45
41
  /**
46
42
  * Empties the jars the runtime owns and expires what it mirrored, which is
47
43
  * what clearing the page's cookie storage would do.
48
44
  *
49
- * As much a part of a rewind as the session store is: emptying the store
50
- * while a jar still holds the token for one of its sessions leaves the next
51
- * call carrying a session that no longer exists, which reads as signed in
52
- * until the answer says sessionExpired.
45
+ * Part of a rewind as much as the session store is: emptying the store
46
+ * while a jar still holds one of its session tokens leaves the next call
47
+ * carrying a dead session, which reads as signed in until the answer says
48
+ * sessionExpired.
53
49
  */
54
50
  reset(): void;
55
51
  }