lambder 7.2.5 → 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 (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -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 +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -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 +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -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 +161 -69
  41. package/dist/core/Lambder.js +370 -226
  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 +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  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 +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  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 +3 -1
  93. package/dist/mock.js +5 -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 +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  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/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -10,14 +10,12 @@ import { LambderExpiringMap, LambderExpiringMapFullError } from "../shared/util/
10
10
  * path can be exercised; unbounded by default. `now` is injectable so a test
11
11
  * can expire a claim without waiting.
12
12
  *
13
- * `maxEntries` is the map's ceiling, 100,000 by default, and it is the one
14
- * way this store differs from a table: a process cannot hold records without
15
- * bound, so past the ceiling the SETTLED records are dropped, soonest expiry
16
- * first, and a retry whose record was dropped executes again instead of
17
- * replaying. Pending claims are never dropped for room, because losing one
18
- * lets two concurrent retries execute at once, which is the thing idempotency
19
- * exists to prevent; a claim that cannot be made room for is reported as
20
- * "pending" instead, so the duplicate is refused rather than run.
13
+ * `maxEntries` (100,000 by default) is the one way this store differs from a
14
+ * table: past the ceiling, SETTLED records are dropped, soonest expiry first,
15
+ * and a retry whose record was dropped executes again instead of replaying.
16
+ * Pending claims are never dropped, since losing one lets two concurrent
17
+ * retries both execute; a claim that finds no room is reported as "pending"
18
+ * instead, so the duplicate is refused rather than run.
21
19
  */
22
20
  export class LambderMemoryIdempotencyStore {
23
21
  records;
@@ -34,7 +32,7 @@ export class LambderMemoryIdempotencyStore {
34
32
  nowSeconds() { return Math.floor(this.now() / 1000); }
35
33
  /** A settled record as the engine reads it: a copy, so a caller writing onto what it got back cannot rewrite the record. */
36
34
  static answerOf(record) {
37
- return { statusCode: record.statusCode, headers: structuredClone(record.headers), body: record.body };
35
+ return { statusCode: record.statusCode, headers: structuredClone(record.headers), body: record.body, fingerprint: record.fingerprint };
38
36
  }
39
37
  async peek(scopeKey) {
40
38
  const record = this.records.get(scopeKey);
@@ -42,43 +40,45 @@ export class LambderMemoryIdempotencyStore {
42
40
  return null;
43
41
  return LambderMemoryIdempotencyStore.answerOf(record);
44
42
  }
45
- async begin(scopeKey, { pendingTtlSeconds }) {
43
+ async begin(scopeKey, { pendingTtlSeconds, fingerprint }) {
46
44
  const existing = this.records.get(scopeKey);
47
45
  if (existing) {
48
46
  if (existing.state === "done")
49
47
  return { state: "done", ...LambderMemoryIdempotencyStore.answerOf(existing) };
50
- return { state: "pending" };
48
+ return { state: "pending", fingerprint: existing.fingerprint };
51
49
  }
52
50
  this.ownerCounter += 1;
53
51
  const ownerToken = `owner-${this.ownerCounter}`;
54
52
  try {
55
- // Not evictable: the claim is the only thing standing between two
56
- // concurrent retries and two executions, and it is also the entry
57
- // the soonest-expiry rule would otherwise take first, since a
58
- // claim lives for minutes and the record it becomes for a day.
59
- this.records.set(scopeKey, { state: "pending", ownerToken }, this.nowSeconds() + pendingTtlSeconds, { evictable: false });
53
+ // Not evictable: the claim is all that stands between two
54
+ // concurrent retries and two executions, and the soonest-expiry
55
+ // rule would take it first (a claim lives minutes, a record a day).
56
+ this.records.set(scopeKey, { state: "pending", ownerToken, fingerprint }, this.nowSeconds() + pendingTtlSeconds, { evictable: false });
60
57
  }
61
58
  catch (error) {
62
59
  if (!(error instanceof LambderExpiringMapFullError))
63
60
  throw error;
64
- // Every record held is a live claim, so there is no room and
65
- // nothing safe to take. "pending" is the interface's shape for
66
- // "the claim is not yours": the caller refuses the duplicate,
67
- // which is the answer that cannot execute anything twice, and a
68
- // retry once the flood drains claims normally.
61
+ // Every record held is a live claim, so nothing is safe to take.
62
+ // "pending" means "the claim is not yours": the caller refuses the
63
+ // duplicate, which cannot execute anything twice, and a retry
64
+ // after the flood drains claims normally. It carries the caller's
65
+ // own fingerprint, so the engine answers the in-flight 409 and the
66
+ // client retries under the same key; any other fingerprint would
67
+ // be the key-reused 409, which moves a client's key scope on as
68
+ // though the operation were settled.
69
69
  if (!this.claimCeilingReported) {
70
70
  this.claimCeilingReported = true;
71
71
  console.error("LambderMemoryIdempotencyStore: every record held is a live claim, so new requests are refused as duplicates until one settles. Raise maxEntries or move to LambderDdbIdempotencyStore.");
72
72
  }
73
- return { state: "pending" };
73
+ return { state: "pending", fingerprint };
74
74
  }
75
75
  this.claimCeilingReported = false;
76
76
  return { state: "new", ownerToken };
77
77
  }
78
- async complete(scopeKey, ownerToken, { statusCode, headers, body, ttlSeconds }) {
79
- // Size first, ownership second, the order LambderDdbIdempotencyStore works
80
- // in: it settles with one conditional write, so the only thing it can
81
- // decide before going to the table is whether the body fits.
78
+ async complete(scopeKey, ownerToken, { statusCode, headers, body, fingerprint, ttlSeconds }) {
79
+ // Size first, ownership second, as LambderDdbIdempotencyStore must:
80
+ // it settles with one conditional write, so whether the body fits is
81
+ // all it can decide before going to the table.
82
82
  if (new TextEncoder().encode(body).length > this.maxBodyBytes)
83
83
  return "too-large";
84
84
  const existing = this.records.get(scopeKey);
@@ -87,18 +87,19 @@ export class LambderMemoryIdempotencyStore {
87
87
  // Settled records ARE evictable: the claim's job is done, and a record
88
88
  // the ceiling drops costs a retry its replay, not its exclusivity.
89
89
  // The key is already held, so this write cannot cross the ceiling.
90
- this.records.set(scopeKey, { state: "done", ownerToken, statusCode, headers: structuredClone(headers), body }, this.nowSeconds() + ttlSeconds);
90
+ this.records.set(scopeKey, { state: "done", ownerToken, statusCode, headers: structuredClone(headers), body, fingerprint }, this.nowSeconds() + ttlSeconds);
91
91
  return "stored";
92
92
  }
93
+ /** Releases a pending claim for its owner; a settled record stays, as LambderIdempotencyStore requires. */
93
94
  async abandon(scopeKey, ownerToken) {
94
95
  const existing = this.records.get(scopeKey);
95
- if (existing && existing.ownerToken === ownerToken)
96
+ if (existing?.state === "pending" && existing.ownerToken === ownerToken)
96
97
  this.records.delete(scopeKey);
97
98
  }
98
99
  /**
99
100
  * The record under a scope, for assertions; null when absent or expired.
100
- * A copy, like every other read here, so an assertion that pokes at what
101
- * it got back cannot edit the stored record.
101
+ * A copy, like every read here, so an assertion cannot edit the stored
102
+ * record.
102
103
  */
103
104
  recordOf(scopeKey) {
104
105
  const record = this.records.get(scopeKey);
@@ -9,15 +9,13 @@ import { type LambderRateLimiter, type LambderRateLimitPolicy, type LambderRateL
9
9
  * `now` is injectable so a test can move time forward and watch a window
10
10
  * reset without waiting for it.
11
11
  *
12
- * `maxEntries` is the ceiling on counters held at once, 100,000 by default,
13
- * and it is the one way this limiter differs from the table: a process cannot
14
- * hold counters without bound, so past the ceiling the counters closest to
15
- * their window's end are dropped and a key whose counter was dropped starts
16
- * that window again from zero. It takes one distinct key per counter to get
17
- * there (a limit keyed per IP under a flood from many of them), and the
18
- * counters with the most life left, which are the long-window ones, are the
19
- * last to go. A deployment where that matters wants LambderDdbRateLimiter,
20
- * whose counters are not held in the process at all.
12
+ * `maxEntries` (100,000 counters by default) is the one way this limiter
13
+ * differs from the table: past the ceiling, the counters closest to their
14
+ * window's end are dropped, and a key whose counter was dropped starts that
15
+ * window again from zero. Reaching it takes one distinct key per counter (a
16
+ * per-IP limit under a flood from many IPs), and the long-window counters,
17
+ * with the most life left, go last. Where that matters, use
18
+ * LambderDdbRateLimiter, whose counters live outside the process.
21
19
  */
22
20
  export declare class LambderMemoryRateLimiter implements LambderRateLimiter {
23
21
  private readonly counters;
@@ -26,6 +24,7 @@ export declare class LambderMemoryRateLimiter implements LambderRateLimiter {
26
24
  now?: () => number;
27
25
  maxEntries?: number;
28
26
  });
27
+ clockMilliseconds(): number;
29
28
  isRateLimited(trackerKey: string, policy: LambderRateLimitPolicy): Promise<LambderRateLimitResult>;
30
29
  /** The attempts counted so far for a key in the current window of `window`; for assertions. */
31
30
  countOf(trackerKey: string, window: LambderRateLimitWindow): number;
@@ -1,6 +1,6 @@
1
1
  import { RATE_LIMIT_WINDOWS, } from "../shared/contracts/LambderRateLimiter.js";
2
2
  import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
3
- import { joinKeyFields } from "../shared/util/LambderKeyFields.js";
3
+ import { joinKeyFields } from "../shared/util/joinKeyFields.js";
4
4
  /**
5
5
  * Fixed-window rate limiter held in memory: the same windows, the same
6
6
  * attempts-count semantics and the same evaluation order as
@@ -11,15 +11,13 @@ import { joinKeyFields } from "../shared/util/LambderKeyFields.js";
11
11
  * `now` is injectable so a test can move time forward and watch a window
12
12
  * reset without waiting for it.
13
13
  *
14
- * `maxEntries` is the ceiling on counters held at once, 100,000 by default,
15
- * and it is the one way this limiter differs from the table: a process cannot
16
- * hold counters without bound, so past the ceiling the counters closest to
17
- * their window's end are dropped and a key whose counter was dropped starts
18
- * that window again from zero. It takes one distinct key per counter to get
19
- * there (a limit keyed per IP under a flood from many of them), and the
20
- * counters with the most life left, which are the long-window ones, are the
21
- * last to go. A deployment where that matters wants LambderDdbRateLimiter,
22
- * whose counters are not held in the process at all.
14
+ * `maxEntries` (100,000 counters by default) is the one way this limiter
15
+ * differs from the table: past the ceiling, the counters closest to their
16
+ * window's end are dropped, and a key whose counter was dropped starts that
17
+ * window again from zero. Reaching it takes one distinct key per counter (a
18
+ * per-IP limit under a flood from many IPs), and the long-window counters,
19
+ * with the most life left, go last. Where that matters, use
20
+ * LambderDdbRateLimiter, whose counters live outside the process.
23
21
  */
24
22
  export class LambderMemoryRateLimiter {
25
23
  counters;
@@ -28,6 +26,9 @@ export class LambderMemoryRateLimiter {
28
26
  this.now = options.now ?? (() => Date.now());
29
27
  this.counters = new LambderExpiringMap({ now: this.now, maxEntries: options.maxEntries });
30
28
  }
29
+ clockMilliseconds() {
30
+ return this.now();
31
+ }
31
32
  async isRateLimited(trackerKey, policy) {
32
33
  const nowSeconds = Math.floor(this.now() / 1000);
33
34
  for (const { key, seconds } of RATE_LIMIT_WINDOWS) {
@@ -35,9 +36,9 @@ export class LambderMemoryRateLimiter {
35
36
  if (!limit)
36
37
  continue;
37
38
  const windowStart = Math.floor(nowSeconds / seconds) * seconds;
38
- // The table gives the tracker key and the window separate
39
- // attributes; one string here has to keep them as distinct, so the
40
- // fields are joined through the escaping join rather than glued.
39
+ // The table keeps the tracker key and the window in separate
40
+ // attributes; one string has to keep them as distinct, hence the
41
+ // escaping join.
41
42
  const counterKey = joinKeyFields(trackerKey, key, String(windowStart));
42
43
  const counter = this.counters.get(counterKey) ?? { count: 0 };
43
44
  // The DynamoDB limiter's conditional ADD: allowed while the count
@@ -1,4 +1,4 @@
1
- import type { LambderSessionRecord, LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
1
+ import type { LambderSessionChanges, LambderSessionRecord, LambderSessionStore, LambderSessionUpdateResult } from "../shared/contracts/LambderSessionStore.js";
2
2
  /**
3
3
  * Session records held in memory: a Map in place of the DynamoDB table. For
4
4
  * tests and for the mock runtime, where the session model (tokens, expiry,
@@ -7,9 +7,11 @@ import type { LambderSessionRecord, LambderSessionStore } from "../shared/contra
7
7
  *
8
8
  * Records are copied on the way in and out through JSON, the way a real store
9
9
  * serializes them, so a caller mutating a fetched record changes nothing until
10
- * it is put back, and data DynamoDB would reject is rejected here too. A
11
- * record also leaves at its own expiresAt, the way the table's TTL removes
12
- * it, so a long-lived process does not hold every session it ever issued.
10
+ * it is written back, and data DynamoDB would reject is rejected here too.
11
+ * Creates and updates carry the table's conditions: a create refuses an
12
+ * existing record, and an update never brings back a deleted one. A record
13
+ * also leaves at its own expiresAt, as the table's TTL removes it, so a
14
+ * long-lived process does not hold every session it ever issued.
13
15
  */
14
16
  export declare class LambderMemorySessionStore<SessionData = unknown> implements LambderSessionStore<SessionData> {
15
17
  /** Nothing here outlives the process. */
@@ -28,17 +30,18 @@ export declare class LambderMemorySessionStore<SessionData = unknown> implements
28
30
  private static keyOf;
29
31
  /**
30
32
  * A record the way a real store hands it back: through JSON, so an
31
- * undefined field is gone and a cyclic value throws here exactly as it
32
- * throws on the way into DynamoDB. structuredClone did neither, which made
33
- * the memory store the more forgiving of the two and let a test pass over
34
- * data the production store would reject.
33
+ * undefined field is gone and a cyclic value throws here exactly as on the
34
+ * way into DynamoDB. structuredClone would do neither, and a test could
35
+ * then pass over data the production store rejects.
35
36
  */
36
37
  private static storedCopy;
37
38
  get(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
38
- put(record: LambderSessionRecord<SessionData>): Promise<void>;
39
- delete(sessionKeyHash: string, secretHash: string): Promise<void>;
39
+ create(record: LambderSessionRecord<SessionData>): Promise<void>;
40
+ update(sessionKeyHash: string, secretHash: string, changes: LambderSessionChanges<SessionData>, condition?: {
41
+ dataVersion: number;
42
+ }): Promise<LambderSessionUpdateResult>;
43
+ delete(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
40
44
  listSecretHashes(sessionKeyHash: string): Promise<string[]>;
41
- markDataExpired(sessionKeyHash: string, secretHash: string, at: number): Promise<void>;
42
45
  /** Every live record held, for assertions. */
43
46
  list(): LambderSessionRecord<SessionData>[];
44
47
  /** Number of live records held. */
@@ -1,5 +1,5 @@
1
1
  import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
2
- import { joinKeyFields } from "../shared/util/LambderKeyFields.js";
2
+ import { joinKeyFields } from "../shared/util/joinKeyFields.js";
3
3
  /**
4
4
  * Session records held in memory: a Map in place of the DynamoDB table. For
5
5
  * tests and for the mock runtime, where the session model (tokens, expiry,
@@ -8,9 +8,11 @@ import { joinKeyFields } from "../shared/util/LambderKeyFields.js";
8
8
  *
9
9
  * Records are copied on the way in and out through JSON, the way a real store
10
10
  * serializes them, so a caller mutating a fetched record changes nothing until
11
- * it is put back, and data DynamoDB would reject is rejected here too. A
12
- * record also leaves at its own expiresAt, the way the table's TTL removes
13
- * it, so a long-lived process does not hold every session it ever issued.
11
+ * it is written back, and data DynamoDB would reject is rejected here too.
12
+ * Creates and updates carry the table's conditions: a create refuses an
13
+ * existing record, and an update never brings back a deleted one. A record
14
+ * also leaves at its own expiresAt, as the table's TTL removes it, so a
15
+ * long-lived process does not hold every session it ever issued.
14
16
  */
15
17
  export class LambderMemorySessionStore {
16
18
  /** Nothing here outlives the process. */
@@ -26,17 +28,16 @@ export class LambderMemorySessionStore {
26
28
  this.records = new LambderExpiringMap(options);
27
29
  }
28
30
  static keyOf(sessionKeyHash, secretHash) {
29
- // Escaped rather than joined plainly: both halves are hex today, but a
30
- // custom LambderSessionCrypto writes whatever it likes into them, and
31
- // two records sharing one key would be one session silently.
31
+ // Escaped rather than joined plainly: the default crypto writes hex,
32
+ // but a custom LambderSessionCrypto may write anything, and two
33
+ // records sharing one key would silently be one session.
32
34
  return joinKeyFields(sessionKeyHash, secretHash);
33
35
  }
34
36
  /**
35
37
  * A record the way a real store hands it back: through JSON, so an
36
- * undefined field is gone and a cyclic value throws here exactly as it
37
- * throws on the way into DynamoDB. structuredClone did neither, which made
38
- * the memory store the more forgiving of the two and let a test pass over
39
- * data the production store would reject.
38
+ * undefined field is gone and a cyclic value throws here exactly as on the
39
+ * way into DynamoDB. structuredClone would do neither, and a test could
40
+ * then pass over data the production store rejects.
40
41
  */
41
42
  static storedCopy(record) {
42
43
  return JSON.parse(JSON.stringify(record));
@@ -45,22 +46,40 @@ export class LambderMemorySessionStore {
45
46
  const record = this.records.get(LambderMemorySessionStore.keyOf(sessionKeyHash, secretHash));
46
47
  return record ? LambderMemorySessionStore.storedCopy(record) : null;
47
48
  }
48
- async put(record) {
49
- this.records.set(LambderMemorySessionStore.keyOf(record.sessionKeyHash, record.secretHash), LambderMemorySessionStore.storedCopy(record), record.expiresAt);
49
+ async create(record) {
50
+ const key = LambderMemorySessionStore.keyOf(record.sessionKeyHash, record.secretHash);
51
+ if (this.records.get(key))
52
+ throw new Error("LambderMemorySessionStore: a session already exists under these hashes.");
53
+ this.records.set(key, LambderMemorySessionStore.storedCopy(record), record.expiresAt);
54
+ }
55
+ async update(sessionKeyHash, secretHash, changes, condition) {
56
+ const key = LambderMemorySessionStore.keyOf(sessionKeyHash, secretHash);
57
+ const record = this.records.get(key);
58
+ if (!record)
59
+ return "missing";
60
+ if (condition && record.dataVersion !== condition.dataVersion)
61
+ return "stale";
62
+ // A write of the data or its deadline moves the version, whatever
63
+ // value it writes, as the DynamoDB store's ADD does.
64
+ const writesData = "data" in changes || changes.dataExpiresAt !== undefined;
65
+ // Through the same JSON copy a create takes, so an update carries
66
+ // exactly what the table would hold, and the record's own expiry
67
+ // moves with it, the way the table's TTL attribute does.
68
+ const updated = LambderMemorySessionStore.storedCopy({ ...record, ...changes, dataVersion: record.dataVersion + (writesData ? 1 : 0) });
69
+ this.records.set(key, updated, updated.expiresAt);
70
+ return "updated";
50
71
  }
51
72
  async delete(sessionKeyHash, secretHash) {
52
- this.records.delete(LambderMemorySessionStore.keyOf(sessionKeyHash, secretHash));
73
+ const key = LambderMemorySessionStore.keyOf(sessionKeyHash, secretHash);
74
+ const record = this.records.get(key);
75
+ this.records.delete(key);
76
+ return record ? LambderMemorySessionStore.storedCopy(record) : null;
53
77
  }
54
78
  async listSecretHashes(sessionKeyHash) {
55
79
  return this.records.values()
56
80
  .filter((record) => record.sessionKeyHash === sessionKeyHash)
57
81
  .map((record) => record.secretHash);
58
82
  }
59
- async markDataExpired(sessionKeyHash, secretHash, at) {
60
- const record = this.records.get(LambderMemorySessionStore.keyOf(sessionKeyHash, secretHash));
61
- if (record)
62
- record.dataExpiresAt = at;
63
- }
64
83
  /** Every live record held, for assertions. */
65
84
  list() {
66
85
  return this.records.values().map((record) => LambderMemorySessionStore.storedCopy(record));
@@ -12,24 +12,39 @@ export type LambderS3FileSourceOptions = {
12
12
  * `{ region: "auto", endpoint, credentials }`.
13
13
  */
14
14
  clientConfig?: S3ClientConfig;
15
+ /**
16
+ * The S3 error names that mean "no such file", read as null so the
17
+ * request falls through. Default: NoSuchKey and NotFound (the key does
18
+ * not exist), AccessDenied (what S3 answers a reader without
19
+ * s3:ListBucket for a missing key, since it will not say whether the key
20
+ * exists), and the refusals of a key S3 will not look up at all:
21
+ * KeyTooLongError and InvalidURI from S3, InvalidObjectName from R2. A
22
+ * reader granted s3:ListBucket gets NoSuchKey for a missing key, so a
23
+ * list without AccessDenied makes a refused credential surface as the
24
+ * error it is.
25
+ */
26
+ notFoundErrorNames?: readonly string[];
15
27
  };
16
28
  /**
17
29
  * Files from an S3 bucket, or any S3-compatible store such as Cloudflare
18
30
  * R2 (pass its endpoint in clientConfig). Needs @aws-sdk/client-s3, an
19
31
  * optional peer dependency loaded on first read, so apps that serve from a
20
- * folder never load it. A missing object reads as null and the request
21
- * falls through; grant s3:ListBucket besides s3:GetObject, otherwise S3
22
- * answers a missing key with AccessDenied, which propagates as an error.
23
- * An object's Content-Type is used unless it is a generic octet-stream, in
24
- * which case the extension decides, as for local files.
32
+ * folder never load it. A missing object (see `notFoundErrorNames`) reads as
33
+ * null and the request falls through; any other failure throws. A request
34
+ * path names the key, so a key that is missing or that S3 refuses to look
35
+ * up is the visitor's doing, and reading it as an error would answer every
36
+ * such path with a 500 before an SPA's shell could be served. An object's
37
+ * Content-Type is used unless it is a generic octet-stream, in which case
38
+ * the extension decides, as for local files.
25
39
  */
26
40
  export declare class LambderS3FileSource implements LambderFileSource {
27
41
  private readonly bucket;
28
42
  private readonly prefix;
29
43
  private readonly clientConfig;
44
+ private readonly notFoundErrorNames;
30
45
  private client;
31
46
  private sdk;
32
- constructor({ bucket, prefix, client, clientConfig }: LambderS3FileSourceOptions);
47
+ constructor({ bucket, prefix, client, clientConfig, notFoundErrorNames }: LambderS3FileSourceOptions);
33
48
  private loadSdk;
34
49
  read(relativePath: string): Promise<LambderFile | null>;
35
50
  }
@@ -1,27 +1,32 @@
1
1
  import { remoteStoreFile } from "../shared/contracts/LambderFileSource.js";
2
+ const DEFAULT_NOT_FOUND_ERROR_NAMES = ["NoSuchKey", "NotFound", "AccessDenied", "KeyTooLongError", "InvalidURI", "InvalidObjectName"];
2
3
  /**
3
4
  * Files from an S3 bucket, or any S3-compatible store such as Cloudflare
4
5
  * R2 (pass its endpoint in clientConfig). Needs @aws-sdk/client-s3, an
5
6
  * optional peer dependency loaded on first read, so apps that serve from a
6
- * folder never load it. A missing object reads as null and the request
7
- * falls through; grant s3:ListBucket besides s3:GetObject, otherwise S3
8
- * answers a missing key with AccessDenied, which propagates as an error.
9
- * An object's Content-Type is used unless it is a generic octet-stream, in
10
- * which case the extension decides, as for local files.
7
+ * folder never load it. A missing object (see `notFoundErrorNames`) reads as
8
+ * null and the request falls through; any other failure throws. A request
9
+ * path names the key, so a key that is missing or that S3 refuses to look
10
+ * up is the visitor's doing, and reading it as an error would answer every
11
+ * such path with a 500 before an SPA's shell could be served. An object's
12
+ * Content-Type is used unless it is a generic octet-stream, in which case
13
+ * the extension decides, as for local files.
11
14
  */
12
15
  export class LambderS3FileSource {
13
16
  bucket;
14
17
  prefix;
15
18
  clientConfig;
19
+ notFoundErrorNames;
16
20
  client;
17
21
  sdk;
18
- constructor({ bucket, prefix = "", client, clientConfig }) {
22
+ constructor({ bucket, prefix = "", client, clientConfig, notFoundErrorNames = DEFAULT_NOT_FOUND_ERROR_NAMES }) {
19
23
  if (!bucket.trim())
20
24
  throw new Error("bucket is required");
21
25
  this.bucket = bucket;
22
26
  this.prefix = prefix;
23
27
  this.client = client;
24
28
  this.clientConfig = clientConfig;
29
+ this.notFoundErrorNames = notFoundErrorNames;
25
30
  }
26
31
  loadSdk() {
27
32
  if (!this.sdk) {
@@ -41,7 +46,7 @@ export class LambderS3FileSource {
41
46
  }
42
47
  catch (err) {
43
48
  const name = err.name;
44
- if (name === "NoSuchKey" || name === "NotFound")
49
+ if (typeof name === "string" && this.notFoundErrorNames.includes(name))
45
50
  return null;
46
51
  throw err;
47
52
  }
@@ -0,0 +1,176 @@
1
+ import type { Context } from "aws-lambda";
2
+ import type Lambder from "../core/Lambder.js";
3
+ import { type LambderHttpEventFormat } from "../core/LambderContext.js";
4
+ import type { LambderHandler } from "../core/LambderCreateOptions.js";
5
+ import type LambderSessionManager from "../session/LambderSessionManager.js";
6
+ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
7
+ import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
8
+ import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
9
+ import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
10
+ import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
11
+ import { LambderTestVisitor, type LambderTestVisitorArgs, type LambderTestVisitorOptions } from "./LambderTestVisitor.js";
12
+ /**
13
+ * What a test app may be told. Nothing is required: `lambderTestApp(lambder)`
14
+ * is a working test app over memory stores.
15
+ *
16
+ * The stores sit where create() takes them (`session.store`,
17
+ * `rateLimits.limiter`, `idempotency.store`, `files`), naming only the part a
18
+ * test replaces; everything else the app configured there stays in force.
19
+ */
20
+ export type LambderTestAppOptions = {
21
+ /** The host visitors browse unless they name their own. Default: "localhost". An app that scopes its session cookie to a domain needs a host under it. */
22
+ host?: string;
23
+ /**
24
+ * The gateway shape the handler is called with: "v2" (an HTTP API, a
25
+ * Function URL) or "v1" (a REST API). Default: "v2". A handler answers
26
+ * both alike through its context, so this matters only to code that reads
27
+ * the raw `ctx.event`, or to a suite that should run on exactly what
28
+ * production delivers.
29
+ */
30
+ eventFormat?: LambderHttpEventFormat;
31
+ /** Default: a fresh LambderMemorySessionStore. Pass your own to run the suite over another store (DynamoDB Local, say). */
32
+ session?: {
33
+ store?: LambderSessionStore<any>;
34
+ };
35
+ /** Default: a fresh LambderMemoryRateLimiter. */
36
+ rateLimits?: {
37
+ limiter?: LambderRateLimiter;
38
+ };
39
+ /** Default: a fresh LambderMemoryIdempotencyStore. */
40
+ idempotency?: {
41
+ store?: LambderIdempotencyStore;
42
+ };
43
+ /** Default: the app's own source, which suits one that reads a local folder. Pass a LambderLocalFileSource over fixtures for an app whose production source is S3 or HTTP. */
44
+ files?: LambderFileSource;
45
+ };
46
+ /**
47
+ * A Lambder instance as a test app takes it: any instance, read for its
48
+ * session data type and, through the ApiContract property rather than the
49
+ * class parameter, for its contract. The property lets a large app name its
50
+ * flattened contract interface explicitly
51
+ * (`lambderTestApp<SessionData, ApiContractType>(lambder)`) and keep the
52
+ * cheap type check that interface exists for.
53
+ */
54
+ export type LambderTestedInstance<TSessionData, TContract> = Lambder<TSessionData, any, any, any, any, any, any, any> & {
55
+ readonly ApiContract: TContract;
56
+ };
57
+ /**
58
+ * A real Lambder app under test: the instance an app already has, with memory
59
+ * stores put under it in place, and as many simulated browsers in front of it
60
+ * as a test needs. No HTTP, no AWS, and nothing in the app restructured.
61
+ *
62
+ * The app's own declarations all run as written: guards, named rate-limit
63
+ * policies, idempotency settings, session salt, cookie options and
64
+ * dataRefresh, hooks and error handlers. Only where things rest is replaced:
65
+ * once this is created, the stores the app was configured with are out of the
66
+ * instance's reach, so a test cannot touch a production table by mistake.
67
+ * What the app reaches on its own (its database, a mailer) is the app's to
68
+ * replace.
69
+ *
70
+ * The sibling of LambderMockApp, which serves a contract from mock handlers:
71
+ * the same verbs (`signIn`, `signOut`, `expireSessionData`, `reset`) over the
72
+ * real handlers.
73
+ *
74
+ * Time is not this class's: fake `Date` with the test runner
75
+ * (`vi.useFakeTimers({ toFake: ["Date"] })`), which moves the framework, the
76
+ * memory stores and the app's own handlers together.
77
+ *
78
+ * One test app per instance: a second one puts its own stores under the same
79
+ * instance and takes over its crash watch, so the first stops seeing either.
80
+ */
81
+ export declare class LambderTestApp<TContract extends LambderApiContractShape = any, TSessionData = any> {
82
+ /** The instance's handler: what `event()` and every visitor call. */
83
+ readonly handler: LambderHandler;
84
+ /** The host visitors browse unless they name their own. */
85
+ readonly host: string;
86
+ /** The session store now under the instance, for assertions; null when the app has no sessions. A LambderMemorySessionStore unless one was given. */
87
+ readonly sessionStore: LambderSessionStore<TSessionData> | null;
88
+ /** The rate limiter now under the instance; null when the app declares no rate limits. */
89
+ readonly rateLimiter: LambderRateLimiter | null;
90
+ /** The idempotency store now under the instance; null when the app has no idempotency. */
91
+ readonly idempotencyStore: LambderIdempotencyStore | null;
92
+ private readonly lambder;
93
+ private readonly wiring;
94
+ /** The memory stores this test app made itself, which reset() empties. One the test supplied is the test's: nothing here knows what else holds it. */
95
+ private readonly ownStores;
96
+ private visitorCount;
97
+ private resetCount;
98
+ private readonly crashList;
99
+ /**
100
+ * The call a crash happened under. The app's 500 says nothing about the
101
+ * crash, so the error travels beside the answer, and with concurrent
102
+ * calls (a duplicate sent while the original is in flight is an ordinary
103
+ * idempotency test) only the async context says which call it belongs to.
104
+ */
105
+ private readonly crashScope;
106
+ constructor(lambder: LambderTestedInstance<TSessionData, TContract>, options?: LambderTestAppOptions);
107
+ /**
108
+ * Every error the app threw while answering a request since the last
109
+ * reset, in order: what reached its global error handler, or the
110
+ * framework's last-resort 500. The answers say nothing about what was
111
+ * thrown, so a test reads it here; `expect(app.crashes).toEqual([])`
112
+ * says nothing crashed. A refusal is not a crash, and neither is an
113
+ * error an `event()` rejects with, which the test already holds.
114
+ */
115
+ get crashes(): readonly Error[];
116
+ /** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
117
+ get sessionManager(): LambderSessionManager<TSessionData>;
118
+ /**
119
+ * A new simulated browser: its own cookie jar, and its own address unless
120
+ * one is given. A stranger until it signs in, through the app's own login
121
+ * API or through `signIn`.
122
+ */
123
+ visitor<TProvidedGuards extends string = never>(...[options]: LambderTestVisitorArgs<LambderTestVisitorOptions<TContract, TProvidedGuards>, TProvidedGuards>): LambderTestVisitor<TContract, TSessionData, TProvidedGuards>;
124
+ /**
125
+ * A new visitor, already signed in: `visitor()` followed by its
126
+ * `signIn()`. The session is minted by the app's own session model, so
127
+ * no login endpoint has to exist or be called.
128
+ *
129
+ * ```typescript
130
+ * const admin = await app.signIn("user:ada", { userId: "ada", role: "admin" });
131
+ * expect(await admin.api("org.rename", { name })).toEqual({ ok: true });
132
+ * ```
133
+ */
134
+ signIn<TProvidedGuards extends string = never>(sessionKey: string, data: TSessionData, ...[options]: LambderTestVisitorArgs<LambderTestVisitorOptions<TContract, TProvidedGuards> & {
135
+ ttlSeconds?: number;
136
+ }, TProvidedGuards>): Promise<LambderTestVisitor<TContract, TSessionData, TProvidedGuards>>;
137
+ /**
138
+ * Ends every session of the subject, the way "log out everywhere" does.
139
+ * A visitor signed in as that subject keeps its cookies, as a browser
140
+ * would, so its next call is what a real one's would be: answered
141
+ * sessionExpired, with the stale cookies evicted.
142
+ */
143
+ signOut(sessionKey: string): Promise<void>;
144
+ /** Marks the subject's session data stale, so the next read renews it through the app's dataRefresh. */
145
+ expireSessionData(sessionKey: string): Promise<void>;
146
+ /**
147
+ * Hands the handler an event that is not an HTTP request (a schedule, an
148
+ * SNS or SQS delivery), which is how an addAction handler runs, with a
149
+ * Lambda context filled in. Resolves to whatever the action returned.
150
+ *
151
+ * ```typescript
152
+ * await app.event({ source: "aws.events", "detail-type": "Scheduled Event" });
153
+ * ```
154
+ */
155
+ event(event: unknown, context?: Partial<Context>): Promise<unknown>;
156
+ /**
157
+ * Rewinds what accumulated: sessions, rate-limit counters and replay
158
+ * records in the stores this test app made, the crashes it recorded, and
159
+ * the cookies of every visitor it created (each empties its jar the next
160
+ * time it is used). For a beforeEach. The app's own data (its database)
161
+ * is the app's to rewind.
162
+ */
163
+ reset(): void;
164
+ }
165
+ /**
166
+ * Puts a built Lambder instance under test. See LambderTestApp.
167
+ *
168
+ * ```typescript
169
+ * import { lambderTestApp } from "lambder/testing";
170
+ * import { lambder } from "../src/index.js";
171
+ *
172
+ * const app = lambderTestApp(lambder);
173
+ * beforeEach(() => app.reset());
174
+ * ```
175
+ */
176
+ export declare const lambderTestApp: <TSessionData = any, TContract extends LambderApiContractShape = any>(lambder: LambderTestedInstance<TSessionData, TContract>, options?: LambderTestAppOptions) => LambderTestApp<TContract, TSessionData>;