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,48 +1,38 @@
1
1
  /**
2
- * A Map whose entries have an expiry, which is the one thing every in-memory
3
- * store here needs: idempotency records, rate-limit counters and session
4
- * records all key something by a string and all stop mattering at a known
5
- * second.
2
+ * A Map whose entries expire, which every in-memory store here needs:
3
+ * idempotency records, rate-limit counters and session records all key
4
+ * something by a string and all stop mattering at a known second.
6
5
  *
7
- * Written once because a store that hand-rolls it tends to expire an entry
8
- * only when something asks for that exact key again, which
9
- * is fine for a test and a slow leak in anything long-lived: a rate-limit
10
- * counter nobody asks about again is dead weight for as long as the process
11
- * runs, and an idempotency record for a key that never returns is dead weight
12
- * for ever. So expiry happens on read AND on an amortized sweep, and no
13
- * caller has to remember either.
6
+ * Entries expire on read and on an amortized sweep, so no caller has to
7
+ * remember either. Expiring only when the same key is read again would leak
8
+ * in anything long-lived: a counter nobody asks about again, or a record for
9
+ * a key that never returns, would stay for as long as the process runs.
14
10
  *
15
- * Expiry alone bounds how long an entry lives, not how many there are: a
16
- * workload that never repeats a key (a rate-limit counter per IP with a
17
- * monthly window, an idempotency key per request) accumulates live entries
18
- * faster than any expiry retires them. So the map also holds a ceiling and
19
- * evicts once it is reached, which is what makes "in memory" a bounded claim
20
- * rather than a slower leak. It evicts whatever expires SOONEST among the
21
- * entries that may be evicted at all, never whatever was written earliest:
22
- * insertion order would retire exactly the entries with the most life left in
23
- * them, which are the long-window rate-limit counters and the day-long
24
- * idempotency records, so a flood of short-lived keys could reset a monthly
25
- * cap. Evicting the soonest-expiring entry costs the caller the least that
26
- * can be taken, and a flood evicts mostly itself.
11
+ * Expiry bounds how long an entry lives, not how many there are: a workload
12
+ * that never repeats a key (a per-IP counter with a monthly window, an
13
+ * idempotency key per request) accumulates live entries faster than expiry
14
+ * retires them. So the map also holds a ceiling, where it evicts the
15
+ * evictable entries that expire soonest. Insertion order would instead
16
+ * retire the entries with the most life left (long-window counters, day-long
17
+ * idempotency records), letting a flood of short-lived keys reset a monthly
18
+ * cap; soonest-to-expire costs the caller least, and a flood evicts mostly
19
+ * itself.
27
20
  *
28
- * "Soonest to expire" is the wrong answer for one kind of entry, though, and
29
- * it is the one where the cost is highest: an idempotency claim lives for a
30
- * few minutes while the settled record it becomes lives for a day, so at the
31
- * ceiling the claim was always the first victim and two concurrent retries
32
- * both executed. An entry written with `evictable: false` is therefore never
33
- * chosen, and a caller whose write cannot be made room for is told so
34
- * (LambderExpiringMapFullError) rather than quietly costing somebody else
21
+ * An idempotency claim lives minutes while the record it settles into lives
22
+ * a day, so at the ceiling it would always be the first victim and two
23
+ * concurrent retries would both execute. An entry written with `evictable:
24
+ * false` is never chosen, and a write that cannot be made room for is
25
+ * refused (LambderExpiringMapFullError) rather than costing somebody else
35
26
  * their claim.
36
27
  *
37
- * Times are epoch SECONDS, matching the TTL attribute DynamoDB uses, so the
38
- * memory stores and their DynamoDB counterparts say the same thing.
28
+ * Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
29
+ * stores and their DynamoDB counterparts say the same thing.
39
30
  */
40
31
  /**
41
32
  * Thrown by set() when the map is at its ceiling and every entry it holds is
42
33
  * protected from eviction. The write did not happen and nothing was dropped
43
- * to make room for it: the caller decides what to do about a store that is
44
- * full of live claims, and the claim already held by somebody else is not
45
- * something this map will trade away.
34
+ * to make room for it: the caller decides what to do about a store full of
35
+ * live claims, and a claim somebody else holds is never traded away.
46
36
  */
47
37
  export declare class LambderExpiringMapFullError extends Error {
48
38
  constructor(maxEntries: number);
@@ -97,22 +87,18 @@ export declare class LambderExpiringMap<TValue> {
97
87
  * Drops a batch of the evictable entries closest to expiring, taking the
98
88
  * map from its ceiling down to a floor one batch below it. Reached only
99
89
  * when expiry cannot keep up, which means the keys are not repeating.
100
- * Evicting costs the caller whatever the entry was protecting (a counter
101
- * resets, a stored answer re-executes on retry), so the ones taken are
102
- * always the ones with the least life left: the soonest to expire were
103
- * going to be lost first anyway, and choosing them means a flood of
104
- * short-lived keys cannot evict a long-window counter.
90
+ * Evicting costs the caller whatever the entry protected (a counter
91
+ * resets, a stored answer re-executes on retry), so the entries taken are
92
+ * those with the least life left, and a flood of short-lived keys cannot
93
+ * evict a long-window counter.
105
94
  *
106
- * A batch rather than a single entry because a single one leaves the map
107
- * exactly at its ceiling, so the next write crosses it again and pays for
108
- * another pass: one pass per write, for as long as the flood lasts. The
109
- * headroom this leaves is what the following writes spend. Measured at
110
- * 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
111
- * batches of one percent.
95
+ * A batch rather than one entry, because one leaves the map exactly at
96
+ * its ceiling and the next write pays for another pass: one pass per
97
+ * write for as long as the flood lasts. At 100,000 entries that is 0.56
98
+ * ms per write, against 0.007 ms with batches of one percent.
112
99
  *
113
- * The pass is linear and the sort is over the evictable entries only,
114
- * which is affordable because reaching the ceiling at all is already
115
- * pathological.
100
+ * The pass is linear and the sort covers only evictable entries, which is
101
+ * affordable because reaching the ceiling at all is already pathological.
116
102
  */
117
103
  private evictBatch;
118
104
  private sweep;
@@ -1,65 +1,53 @@
1
1
  /**
2
- * A Map whose entries have an expiry, which is the one thing every in-memory
3
- * store here needs: idempotency records, rate-limit counters and session
4
- * records all key something by a string and all stop mattering at a known
5
- * second.
2
+ * A Map whose entries expire, which every in-memory store here needs:
3
+ * idempotency records, rate-limit counters and session records all key
4
+ * something by a string and all stop mattering at a known second.
6
5
  *
7
- * Written once because a store that hand-rolls it tends to expire an entry
8
- * only when something asks for that exact key again, which
9
- * is fine for a test and a slow leak in anything long-lived: a rate-limit
10
- * counter nobody asks about again is dead weight for as long as the process
11
- * runs, and an idempotency record for a key that never returns is dead weight
12
- * for ever. So expiry happens on read AND on an amortized sweep, and no
13
- * caller has to remember either.
6
+ * Entries expire on read and on an amortized sweep, so no caller has to
7
+ * remember either. Expiring only when the same key is read again would leak
8
+ * in anything long-lived: a counter nobody asks about again, or a record for
9
+ * a key that never returns, would stay for as long as the process runs.
14
10
  *
15
- * Expiry alone bounds how long an entry lives, not how many there are: a
16
- * workload that never repeats a key (a rate-limit counter per IP with a
17
- * monthly window, an idempotency key per request) accumulates live entries
18
- * faster than any expiry retires them. So the map also holds a ceiling and
19
- * evicts once it is reached, which is what makes "in memory" a bounded claim
20
- * rather than a slower leak. It evicts whatever expires SOONEST among the
21
- * entries that may be evicted at all, never whatever was written earliest:
22
- * insertion order would retire exactly the entries with the most life left in
23
- * them, which are the long-window rate-limit counters and the day-long
24
- * idempotency records, so a flood of short-lived keys could reset a monthly
25
- * cap. Evicting the soonest-expiring entry costs the caller the least that
26
- * can be taken, and a flood evicts mostly itself.
11
+ * Expiry bounds how long an entry lives, not how many there are: a workload
12
+ * that never repeats a key (a per-IP counter with a monthly window, an
13
+ * idempotency key per request) accumulates live entries faster than expiry
14
+ * retires them. So the map also holds a ceiling, where it evicts the
15
+ * evictable entries that expire soonest. Insertion order would instead
16
+ * retire the entries with the most life left (long-window counters, day-long
17
+ * idempotency records), letting a flood of short-lived keys reset a monthly
18
+ * cap; soonest-to-expire costs the caller least, and a flood evicts mostly
19
+ * itself.
27
20
  *
28
- * "Soonest to expire" is the wrong answer for one kind of entry, though, and
29
- * it is the one where the cost is highest: an idempotency claim lives for a
30
- * few minutes while the settled record it becomes lives for a day, so at the
31
- * ceiling the claim was always the first victim and two concurrent retries
32
- * both executed. An entry written with `evictable: false` is therefore never
33
- * chosen, and a caller whose write cannot be made room for is told so
34
- * (LambderExpiringMapFullError) rather than quietly costing somebody else
21
+ * An idempotency claim lives minutes while the record it settles into lives
22
+ * a day, so at the ceiling it would always be the first victim and two
23
+ * concurrent retries would both execute. An entry written with `evictable:
24
+ * false` is never chosen, and a write that cannot be made room for is
25
+ * refused (LambderExpiringMapFullError) rather than costing somebody else
35
26
  * their claim.
36
27
  *
37
- * Times are epoch SECONDS, matching the TTL attribute DynamoDB uses, so the
38
- * memory stores and their DynamoDB counterparts say the same thing.
28
+ * Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
29
+ * stores and their DynamoDB counterparts say the same thing.
39
30
  */
40
31
  import { assertPositiveInteger } from "./LambderOptionChecks.js";
41
32
  /** How many writes go by before the map walks itself and drops what has expired. */
42
33
  const SWEEP_WRITE_INTERVAL = 256;
43
34
  /**
44
- * Live entries held before the oldest writes start being evicted. High enough
45
- * that no ordinary single-process run reaches it, low enough to bound the
46
- * process: crossing it means a key space that never repeats, where the
47
- * alternative to evicting is growing until the process dies.
35
+ * Live entries held before eviction starts. High enough that no ordinary
36
+ * single-process run reaches it, low enough to bound the process: crossing it
37
+ * means a key space that never repeats, where the alternative to evicting is
38
+ * growing until the process dies.
48
39
  */
49
40
  const DEFAULT_MAX_ENTRIES = 100_000;
50
41
  /**
51
- * Share of the ceiling one eviction pass reclaims. Evicting exactly the one
52
- * entry that crossed the ceiling means every later write crosses it again, so
53
- * the map pays a full pass per write for as long as the flood lasts; taking a
54
- * batch amortizes that pass over the writes the headroom absorbs.
42
+ * Share of the ceiling one eviction pass reclaims, so the pass is amortized
43
+ * over the writes the headroom absorbs (see evictBatch).
55
44
  */
56
45
  const EVICTION_BATCH_SHARE = 0.01;
57
46
  /**
58
47
  * Thrown by set() when the map is at its ceiling and every entry it holds is
59
48
  * protected from eviction. The write did not happen and nothing was dropped
60
- * to make room for it: the caller decides what to do about a store that is
61
- * full of live claims, and the claim already held by somebody else is not
62
- * something this map will trade away.
49
+ * to make room for it: the caller decides what to do about a store full of
50
+ * live claims, and a claim somebody else holds is never traded away.
63
51
  */
64
52
  export class LambderExpiringMapFullError extends Error {
65
53
  constructor(maxEntries) {
@@ -160,22 +148,18 @@ export class LambderExpiringMap {
160
148
  * Drops a batch of the evictable entries closest to expiring, taking the
161
149
  * map from its ceiling down to a floor one batch below it. Reached only
162
150
  * when expiry cannot keep up, which means the keys are not repeating.
163
- * Evicting costs the caller whatever the entry was protecting (a counter
164
- * resets, a stored answer re-executes on retry), so the ones taken are
165
- * always the ones with the least life left: the soonest to expire were
166
- * going to be lost first anyway, and choosing them means a flood of
167
- * short-lived keys cannot evict a long-window counter.
151
+ * Evicting costs the caller whatever the entry protected (a counter
152
+ * resets, a stored answer re-executes on retry), so the entries taken are
153
+ * those with the least life left, and a flood of short-lived keys cannot
154
+ * evict a long-window counter.
168
155
  *
169
- * A batch rather than a single entry because a single one leaves the map
170
- * exactly at its ceiling, so the next write crosses it again and pays for
171
- * another pass: one pass per write, for as long as the flood lasts. The
172
- * headroom this leaves is what the following writes spend. Measured at
173
- * 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
174
- * batches of one percent.
156
+ * A batch rather than one entry, because one leaves the map exactly at
157
+ * its ceiling and the next write pays for another pass: one pass per
158
+ * write for as long as the flood lasts. At 100,000 entries that is 0.56
159
+ * ms per write, against 0.007 ms with batches of one percent.
175
160
  *
176
- * The pass is linear and the sort is over the evictable entries only,
177
- * which is affordable because reaching the ceiling at all is already
178
- * pathological.
161
+ * The pass is linear and the sort covers only evictable entries, which is
162
+ * affordable because reaching the ceiling at all is already pathological.
179
163
  */
180
164
  evictBatch(justWritten) {
181
165
  const floorSize = Math.max(0, this.maxEntries - this.evictionBatchSize);
@@ -8,14 +8,13 @@
8
8
  *
9
9
  * A failed import is only half of "no such thing". The other half is a
10
10
  * bundler: package.json maps fs, path, zlib and crypto to `false` for the
11
- * browser, and webpack, Vite and esbuild each honour that by resolving the
12
- * import to a stub module rather than by rejecting it. Those stubs are
13
- * objects, so a truthiness test calls them usable and the caller dies on the
14
- * first real function it reaches. `expect` names a function the genuine
15
- * module exports; a module that cannot answer it is not the module.
11
+ * browser, and webpack, Vite and esbuild resolve such an import to a stub
12
+ * object rather than rejecting it, so a truthiness test would call the stub
13
+ * usable and the caller would die on its first real call. `expect` names a
14
+ * function the genuine module exports; a module without it is not the module.
16
15
  *
17
- * The answer is memoized either way, absence included, so the probe runs once
18
- * per module however often a request asks for it.
16
+ * The answer, absence included, is memoized, so the probe runs once per
17
+ * module.
19
18
  */
20
19
  const loadNodeModule = (load, expect) => {
21
20
  let pending = null;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The checks every option that names a count, a size or a duration goes
3
- * through at creation, so a bad value is one wording and one predicate
4
- * everywhere rather than seven spellings of the same rule. `name` is the
5
- * option as the reader wrote it (`maxResponseBytes`, `session.ttlSeconds`),
6
- * so the error says which one to fix.
3
+ * through at creation, so a bad value meets one predicate and one wording
4
+ * everywhere. `name` is the option as the reader wrote it
5
+ * (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
6
+ * fix.
7
7
  */
8
8
  /** A safe integer of one or more; returns it so the check reads as an assignment. */
9
9
  export declare const assertPositiveInteger: (value: unknown, name: string) => number;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The checks every option that names a count, a size or a duration goes
3
- * through at creation, so a bad value is one wording and one predicate
4
- * everywhere rather than seven spellings of the same rule. `name` is the
5
- * option as the reader wrote it (`maxResponseBytes`, `session.ttlSeconds`),
6
- * so the error says which one to fix.
3
+ * through at creation, so a bad value meets one predicate and one wording
4
+ * everywhere. `name` is the option as the reader wrote it
5
+ * (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
6
+ * fix.
7
7
  */
8
8
  const describe = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
9
9
  /** A safe integer of one or more; returns it so the check reads as an assignment. */
@@ -2,11 +2,11 @@
2
2
  * Marks an object as a response without anyone having to import the class to
3
3
  * ask. Layers that must recognise one but must not depend on core at runtime
4
4
  * (the guards engine, which runs in the browser too) test for this key.
5
- * Symbol.for keeps it true across realms and across duplicate copies of the
6
- * package. Defined here, in shared, so the class that carries the brand and
7
- * the engine that checks for it import the one constant: a hand-typed copy
8
- * of the symbol's name would keep compiling after a rename while the runtime
9
- * check silently stopped matching.
5
+ * Symbol.for keeps it true across realms and duplicate copies of the
6
+ * package. Defined in shared so the class that carries the brand and the
7
+ * engine that checks it import one constant: a hand-typed copy of the
8
+ * symbol's name would keep compiling after a rename while the runtime check
9
+ * silently stopped matching.
10
10
  */
11
11
  export declare const LAMBDER_RESPONSE_BRAND: unique symbol;
12
12
  /**
@@ -2,11 +2,11 @@
2
2
  * Marks an object as a response without anyone having to import the class to
3
3
  * ask. Layers that must recognise one but must not depend on core at runtime
4
4
  * (the guards engine, which runs in the browser too) test for this key.
5
- * Symbol.for keeps it true across realms and across duplicate copies of the
6
- * package. Defined here, in shared, so the class that carries the brand and
7
- * the engine that checks for it import the one constant: a hand-typed copy
8
- * of the symbol's name would keep compiling after a rename while the runtime
9
- * check silently stopped matching.
5
+ * Symbol.for keeps it true across realms and duplicate copies of the
6
+ * package. Defined in shared so the class that carries the brand and the
7
+ * engine that checks it import one constant: a hand-typed copy of the
8
+ * symbol's name would keep compiling after a rename while the runtime check
9
+ * silently stopped matching.
10
10
  */
11
11
  export const LAMBDER_RESPONSE_BRAND = Symbol.for("lambder.response");
12
12
  /**
@@ -1,9 +1,9 @@
1
1
  /**
2
- * SHA-256 over text through WebCrypto, as hex: the one digest every layer
3
- * shares. The session crypto hashes bearer secrets with it and the
4
- * rate-limit engine folds an over-long tracker key with it, so both key
5
- * spaces are built from the same primitive on every runtime (browsers on a
6
- * secure context, Node 20+, edge runtimes).
2
+ * SHA-256 through WebCrypto: the one digest every layer shares. The session
3
+ * crypto hashes bearer secrets with it and the rate-limit engine folds an
4
+ * over-long tracker key with it, so both key spaces are built from the same
5
+ * primitive on every runtime (browsers on a secure context, Node 20+, edge
6
+ * runtimes); an upload's checksum is the same digest over the file's bytes.
7
7
  */
8
8
  /** Lowercase hex of a byte array, two characters per byte. */
9
9
  export declare const bytesToHexString: (bytes: Uint8Array) => string;
@@ -15,3 +15,5 @@ export declare const bytesToHexString: (bytes: Uint8Array) => string;
15
15
  export declare const resolveWebCrypto: () => Promise<Crypto>;
16
16
  /** The SHA-256 digest of `text` (UTF-8), as 64 lowercase hex characters. */
17
17
  export declare const sha256HexOf: (text: string) => Promise<string>;
18
+ /** The SHA-256 digest of `bytes`, as base64: the form object storage checks an upload's checksum in. */
19
+ export declare const sha256Base64Of: (bytes: Uint8Array) => Promise<string>;
@@ -1,10 +1,11 @@
1
+ import { bytesToBase64 } from "./LambderBase64.js";
1
2
  import { getCrypto } from "./LambderNodeModules.js";
2
3
  /**
3
- * SHA-256 over text through WebCrypto, as hex: the one digest every layer
4
- * shares. The session crypto hashes bearer secrets with it and the
5
- * rate-limit engine folds an over-long tracker key with it, so both key
6
- * spaces are built from the same primitive on every runtime (browsers on a
7
- * secure context, Node 20+, edge runtimes).
4
+ * SHA-256 through WebCrypto: the one digest every layer shares. The session
5
+ * crypto hashes bearer secrets with it and the rate-limit engine folds an
6
+ * over-long tracker key with it, so both key spaces are built from the same
7
+ * primitive on every runtime (browsers on a secure context, Node 20+, edge
8
+ * runtimes); an upload's checksum is the same digest over the file's bytes.
8
9
  */
9
10
  /** Lowercase hex of a byte array, two characters per byte. */
10
11
  export const bytesToHexString = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -32,3 +33,8 @@ export const sha256HexOf = async (text) => {
32
33
  const digest = await webCrypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
33
34
  return bytesToHexString(new Uint8Array(digest));
34
35
  };
36
+ /** The SHA-256 digest of `bytes`, as base64: the form object storage checks an upload's checksum in. */
37
+ export const sha256Base64Of = async (bytes) => {
38
+ const webCrypto = await resolveWebCrypto();
39
+ return bytesToBase64(new Uint8Array(await webCrypto.subtle.digest("SHA-256", bytes)));
40
+ };
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The small type utilities more than one module needs.
3
3
  *
4
- * Nothing here is Lambder's own vocabulary: these are the shapes TypeScript
5
- * does not ship, written once because the alternative is the same three
6
- * lines in every module that wants them, drifting in name and in meaning.
4
+ * Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
5
+ * not ship, written once so they cannot drift in name or meaning across the
6
+ * modules that use them.
7
7
  */
8
8
  /**
9
9
  * A value a caller may hand back either synchronously or as a promise. Every
@@ -18,15 +18,14 @@ export type MaybePromise<T> = T | Promise<T>;
18
18
  *
19
19
  * An all-optional map is inhabited by `{}`, which would let `guards: {}`
20
20
  * satisfy requireSessionApiGuards / requirePublicApiGuards at the type level
21
- * while declaring no guard at all: the option is present, so the required-field
21
+ * while declaring no guard: the option is present, so the required-field
22
22
  * check passes, and it normalizes to zero entries, so nothing runs. Requiring
23
23
  * the chosen key also rejects `{ theGuard: undefined }`, which an optional
24
- * property accepts and which would otherwise reach the guard's handler with an
24
+ * property accepts and which would reach the guard's handler with an
25
25
  * undefined param.
26
26
  *
27
- * Option-neutral, and the rate-limit option's map form is built with the same
28
- * type: the two had drifted, and `rateLimit: {}` compiled while `guards: {}`
29
- * did not.
27
+ * Option-neutral: the rate-limit option's map form uses it too, so
28
+ * `rateLimit: {}` and `guards: {}` are refused alike.
30
29
  */
31
30
  export type LambderNonEmptyOptionMap<TMap> = {
32
31
  [K in keyof TMap]-?: Required<Pick<TMap, K>> & Omit<TMap, K>;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The small type utilities more than one module needs.
3
3
  *
4
- * Nothing here is Lambder's own vocabulary: these are the shapes TypeScript
5
- * does not ship, written once because the alternative is the same three
6
- * lines in every module that wants them, drifting in name and in meaning.
4
+ * Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
5
+ * not ship, written once so they cannot drift in name or meaning across the
6
+ * modules that use them.
7
7
  */
8
8
  export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bounding the caller-supplied field of a tracker or scope key.
3
+ *
4
+ * The rate-limit engine and the idempotency engine each build a store key
5
+ * around a field only the caller controls: a custom rate-limit key or a
6
+ * session key, a callerIdentity or a session key. A store has a key limit of
7
+ * its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
8
+ * past it by throwing, and both engines fail open on a store throw by
9
+ * default: a long enough field (a 3,000-character email, a device token)
10
+ * would turn the rate limit or the idempotency off for that caller, in
11
+ * silence, while the table held the field in plain text. So the field is
12
+ * bounded before any store sees it, in one implementation the two engines
13
+ * share, so they cannot drift apart.
14
+ */
15
+ /**
16
+ * The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
17
+ * hex>` once it does not. The digest keeps distinct callers on distinct
18
+ * counters and scopes, and a value that fits stays readable in the table.
19
+ */
20
+ export declare const boundKeyField: (kind: string, value: string) => Promise<string>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bounding the caller-supplied field of a tracker or scope key.
3
+ *
4
+ * The rate-limit engine and the idempotency engine each build a store key
5
+ * around a field only the caller controls: a custom rate-limit key or a
6
+ * session key, a callerIdentity or a session key. A store has a key limit of
7
+ * its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
8
+ * past it by throwing, and both engines fail open on a store throw by
9
+ * default: a long enough field (a 3,000-character email, a device token)
10
+ * would turn the rate limit or the idempotency off for that caller, in
11
+ * silence, while the table held the field in plain text. So the field is
12
+ * bounded before any store sees it, in one implementation the two engines
13
+ * share, so they cannot drift apart.
14
+ */
15
+ import { joinKeyFields } from "./joinKeyFields.js";
16
+ import { sha256HexOf } from "./LambderTextDigest.js";
17
+ /**
18
+ * The ceiling, in UTF-8 bytes, on the field as it is written into the key;
19
+ * past it, the field is replaced by its digest. Measured escaped, since
20
+ * joinKeyFields doubles every separator and escape character: a field of
21
+ * 1,000 separators is 2,000 bytes in the key. 1024 sits well inside every
22
+ * store's key limit, with the store's prefix and the engine's other fields
23
+ * (API and policy names, a posted idempotency key of at most 200 characters)
24
+ * joined around it.
25
+ */
26
+ const MAX_KEY_FIELD_BYTES = 1024;
27
+ /**
28
+ * The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
29
+ * hex>` once it does not. The digest keeps distinct callers on distinct
30
+ * counters and scopes, and a value that fits stays readable in the table.
31
+ */
32
+ export const boundKeyField = async (kind, value) => new TextEncoder().encode(joinKeyFields(value)).length > MAX_KEY_FIELD_BYTES
33
+ ? `${kind}:h:${await sha256HexOf(value)}`
34
+ : `${kind}:${value}`;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * JSON with object keys sorted at every level, so two values that are the
3
+ * same data hash the same whatever order their keys were built in. Arrays
4
+ * keep their order: a tuple's positions and an enum's values are part of the
5
+ * data. Undefined entries are dropped, as JSON.stringify would drop them.
6
+ *
7
+ * What the API signature digests a schema's description with, and what the
8
+ * idempotency engine fingerprints a request with: two hashes that must not
9
+ * depend on the order a client or a builder happened to write keys in.
10
+ */
11
+ export declare const canonicalJson: (value: unknown) => string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * JSON with object keys sorted at every level, so two values that are the
3
+ * same data hash the same whatever order their keys were built in. Arrays
4
+ * keep their order: a tuple's positions and an enum's values are part of the
5
+ * data. Undefined entries are dropped, as JSON.stringify would drop them.
6
+ *
7
+ * What the API signature digests a schema's description with, and what the
8
+ * idempotency engine fingerprints a request with: two hashes that must not
9
+ * depend on the order a client or a builder happened to write keys in.
10
+ */
11
+ export const canonicalJson = (value) => JSON.stringify(sortKeys(value));
12
+ const sortKeys = (value) => {
13
+ if (Array.isArray(value))
14
+ return value.map(sortKeys);
15
+ if (value === null || typeof value !== "object")
16
+ return value;
17
+ const source = value;
18
+ // No prototype, so a "__proto__" key (JSON.parse makes it an own key) is
19
+ // kept as data. On a plain object the assignment would set the prototype
20
+ // instead, and two payloads differing only under "__proto__" would share
21
+ // one fingerprint.
22
+ const sorted = Object.create(null);
23
+ for (const key of Object.keys(source).sort()) {
24
+ if (source[key] !== undefined)
25
+ sorted[key] = sortKeys(source[key]);
26
+ }
27
+ return sorted;
28
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * The rate limiter and the idempotency engine each join fields with a
5
+ * separator, and at least one field is caller data (a policy's rate-limit
6
+ * key, a posted idempotency key). A plain join lets two field lists produce
7
+ * one string, so two callers would share a counter or one would read
8
+ * another's stored answer. The caller's separator is escaped, not refused: a
9
+ * limit that rejects a legal key is a bug of its own.
10
+ *
11
+ * The join is one-way (a key is looked up or compared, never taken apart),
12
+ * unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
13
+ * must decode to exactly what was written; neither stands in for the other.
14
+ * Both engines share this one implementation so they cannot drift apart.
15
+ */
16
+ /**
17
+ * The fields joined into one key, each escaped, so no two distinct field
18
+ * lists can produce the same string.
19
+ */
20
+ export declare const joinKeyFields: (...fields: string[]) => string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * The rate limiter and the idempotency engine each join fields with a
5
+ * separator, and at least one field is caller data (a policy's rate-limit
6
+ * key, a posted idempotency key). A plain join lets two field lists produce
7
+ * one string, so two callers would share a counter or one would read
8
+ * another's stored answer. The caller's separator is escaped, not refused: a
9
+ * limit that rejects a legal key is a bug of its own.
10
+ *
11
+ * The join is one-way (a key is looked up or compared, never taken apart),
12
+ * unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
13
+ * must decode to exactly what was written; neither stands in for the other.
14
+ * Both engines share this one implementation so they cannot drift apart.
15
+ */
16
+ /** One field, with the separator and its own escape character escaped. */
17
+ const escapeKeyField = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
18
+ /**
19
+ * The fields joined into one key, each escaped, so no two distinct field
20
+ * lists can produce the same string.
21
+ */
22
+ export const joinKeyFields = (...fields) => fields.map(escapeKeyField).join("|");