lambder 6.0.1 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +26 -24
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
@@ -0,0 +1,119 @@
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.
6
+ *
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.
14
+ *
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.
27
+ *
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
35
+ * their claim.
36
+ *
37
+ * Times are epoch SECONDS, matching the TTL attribute DynamoDB uses, so the
38
+ * memory stores and their DynamoDB counterparts say the same thing.
39
+ */
40
+ /**
41
+ * Thrown by set() when the map is at its ceiling and every entry it holds is
42
+ * 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.
46
+ */
47
+ export declare class LambderExpiringMapFullError extends Error {
48
+ constructor(maxEntries: number);
49
+ }
50
+ export declare class LambderExpiringMap<TValue> {
51
+ private readonly entries;
52
+ private readonly now;
53
+ private readonly maxEntries;
54
+ private readonly evictionBatchSize;
55
+ private writesSinceSweep;
56
+ /**
57
+ * `now` is injectable so a test can move time forward without waiting.
58
+ * `maxEntries` caps live entries; once a sweep cannot get back under it,
59
+ * the soonest to expire go first, protected entries excepted.
60
+ */
61
+ constructor(options?: {
62
+ now?: () => number;
63
+ maxEntries?: number;
64
+ });
65
+ private nowSeconds;
66
+ /**
67
+ * The value, or undefined when it is absent or past its expiry. An
68
+ * expired entry is dropped on the way, so a read never resurrects one.
69
+ */
70
+ get(key: string): TValue | undefined;
71
+ /**
72
+ * Stores the value until `expiresAt` (epoch seconds), replacing what was
73
+ * there. `evictable: false` keeps the entry out of the ceiling eviction,
74
+ * for the entries whose loss costs more than the flood that would take
75
+ * them (an idempotency claim, whose loss lets a duplicate execute).
76
+ *
77
+ * Throws LambderExpiringMapFullError when the map is at its ceiling and
78
+ * nothing there may be evicted. `expiresAt` is checked the way the
79
+ * constructor checks `maxEntries`, because a NaN expiry compares false
80
+ * against every clock: an entry carrying one is neither read, swept nor
81
+ * evicted, which is one immortal record per bad TTL.
82
+ */
83
+ set(key: string, value: TValue, expiresAt: number, options?: {
84
+ evictable?: boolean;
85
+ }): void;
86
+ delete(key: string): void;
87
+ /**
88
+ * Every live value. Expired entries are skipped rather than deleted:
89
+ * reading the map is not a reason to write to it, and the amortized sweep
90
+ * on writes is what reclaims them.
91
+ */
92
+ values(): TValue[];
93
+ /** Number of live entries, counted rather than swept, for the same reason values() counts. */
94
+ get size(): number;
95
+ clear(): void;
96
+ /**
97
+ * Drops a batch of the evictable entries closest to expiring, taking the
98
+ * map from its ceiling down to a floor one batch below it. Reached only
99
+ * 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.
105
+ *
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.
112
+ *
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.
116
+ */
117
+ private evictBatch;
118
+ private sweep;
119
+ }
@@ -0,0 +1,217 @@
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.
6
+ *
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.
14
+ *
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.
27
+ *
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
35
+ * their claim.
36
+ *
37
+ * Times are epoch SECONDS, matching the TTL attribute DynamoDB uses, so the
38
+ * memory stores and their DynamoDB counterparts say the same thing.
39
+ */
40
+ import { assertPositiveInteger } from "./LambderOptionChecks.js";
41
+ /** How many writes go by before the map walks itself and drops what has expired. */
42
+ const SWEEP_WRITE_INTERVAL = 256;
43
+ /**
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.
48
+ */
49
+ const DEFAULT_MAX_ENTRIES = 100_000;
50
+ /**
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.
55
+ */
56
+ const EVICTION_BATCH_SHARE = 0.01;
57
+ /**
58
+ * Thrown by set() when the map is at its ceiling and every entry it holds is
59
+ * 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.
63
+ */
64
+ export class LambderExpiringMapFullError extends Error {
65
+ constructor(maxEntries) {
66
+ super(`LambderExpiringMap: at the ceiling of ${maxEntries} entries and every entry is protected from eviction, so the write was refused.`);
67
+ this.name = "LambderExpiringMapFullError";
68
+ }
69
+ }
70
+ export class LambderExpiringMap {
71
+ entries = new Map();
72
+ now;
73
+ maxEntries;
74
+ evictionBatchSize;
75
+ writesSinceSweep = 0;
76
+ /**
77
+ * `now` is injectable so a test can move time forward without waiting.
78
+ * `maxEntries` caps live entries; once a sweep cannot get back under it,
79
+ * the soonest to expire go first, protected entries excepted.
80
+ */
81
+ constructor(options = {}) {
82
+ this.now = options.now ?? (() => Date.now());
83
+ this.maxEntries = assertPositiveInteger(options.maxEntries ?? DEFAULT_MAX_ENTRIES, "maxEntries");
84
+ this.evictionBatchSize = Math.max(1, Math.ceil(this.maxEntries * EVICTION_BATCH_SHARE));
85
+ }
86
+ nowSeconds() { return Math.floor(this.now() / 1000); }
87
+ /**
88
+ * The value, or undefined when it is absent or past its expiry. An
89
+ * expired entry is dropped on the way, so a read never resurrects one.
90
+ */
91
+ get(key) {
92
+ const entry = this.entries.get(key);
93
+ if (!entry)
94
+ return undefined;
95
+ if (entry.expiresAt <= this.nowSeconds()) {
96
+ this.entries.delete(key);
97
+ return undefined;
98
+ }
99
+ return entry.value;
100
+ }
101
+ /**
102
+ * Stores the value until `expiresAt` (epoch seconds), replacing what was
103
+ * there. `evictable: false` keeps the entry out of the ceiling eviction,
104
+ * for the entries whose loss costs more than the flood that would take
105
+ * them (an idempotency claim, whose loss lets a duplicate execute).
106
+ *
107
+ * Throws LambderExpiringMapFullError when the map is at its ceiling and
108
+ * nothing there may be evicted. `expiresAt` is checked the way the
109
+ * constructor checks `maxEntries`, because a NaN expiry compares false
110
+ * against every clock: an entry carrying one is neither read, swept nor
111
+ * evicted, which is one immortal record per bad TTL.
112
+ */
113
+ set(key, value, expiresAt, options = {}) {
114
+ assertPositiveInteger(expiresAt, "expiresAt");
115
+ this.entries.set(key, { value, expiresAt, evictable: options.evictable ?? true });
116
+ this.writesSinceSweep += 1;
117
+ if (this.writesSinceSweep >= SWEEP_WRITE_INTERVAL)
118
+ this.sweep();
119
+ if (this.entries.size <= this.maxEntries)
120
+ return;
121
+ this.evictBatch(key);
122
+ if (this.entries.size > this.maxEntries) {
123
+ // Only a key the map did not already hold can push the size past
124
+ // the ceiling, so dropping the one just written is what leaves the
125
+ // map exactly as the caller found it.
126
+ this.entries.delete(key);
127
+ throw new LambderExpiringMapFullError(this.maxEntries);
128
+ }
129
+ }
130
+ delete(key) { this.entries.delete(key); }
131
+ /**
132
+ * Every live value. Expired entries are skipped rather than deleted:
133
+ * reading the map is not a reason to write to it, and the amortized sweep
134
+ * on writes is what reclaims them.
135
+ */
136
+ values() {
137
+ const nowSeconds = this.nowSeconds();
138
+ const live = [];
139
+ for (const entry of this.entries.values()) {
140
+ if (entry.expiresAt > nowSeconds)
141
+ live.push(entry.value);
142
+ }
143
+ return live;
144
+ }
145
+ /** Number of live entries, counted rather than swept, for the same reason values() counts. */
146
+ get size() {
147
+ const nowSeconds = this.nowSeconds();
148
+ let count = 0;
149
+ for (const entry of this.entries.values()) {
150
+ if (entry.expiresAt > nowSeconds)
151
+ count += 1;
152
+ }
153
+ return count;
154
+ }
155
+ clear() {
156
+ this.entries.clear();
157
+ this.writesSinceSweep = 0;
158
+ }
159
+ /**
160
+ * Drops a batch of the evictable entries closest to expiring, taking the
161
+ * map from its ceiling down to a floor one batch below it. Reached only
162
+ * 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.
168
+ *
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.
175
+ *
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.
179
+ */
180
+ evictBatch(justWritten) {
181
+ const floorSize = Math.max(0, this.maxEntries - this.evictionBatchSize);
182
+ // One clock read per batch: expired entries go first, protected or
183
+ // not, since a claim past its own TTL holds nothing anyone can settle
184
+ // and is never worth a live entry's slot. Reclaimed in the same pass
185
+ // that collects the candidates, so a batch is still one walk.
186
+ const nowSeconds = this.nowSeconds();
187
+ const candidates = [];
188
+ for (const [key, entry] of this.entries) {
189
+ if (entry.expiresAt <= nowSeconds) {
190
+ this.entries.delete(key);
191
+ continue;
192
+ }
193
+ // The entry just written is what the caller asked to store; taking
194
+ // it as its own victim would answer a write that stored nothing.
195
+ if (entry.evictable && key !== justWritten)
196
+ candidates.push({ key, expiresAt: entry.expiresAt });
197
+ }
198
+ if (this.entries.size <= floorSize)
199
+ return;
200
+ // A stable sort over a list built in insertion order, so entries
201
+ // expiring in the same second are taken oldest first.
202
+ candidates.sort((first, second) => first.expiresAt - second.expiresAt);
203
+ for (const candidate of candidates) {
204
+ if (this.entries.size <= floorSize)
205
+ break;
206
+ this.entries.delete(candidate.key);
207
+ }
208
+ }
209
+ sweep() {
210
+ const nowSeconds = this.nowSeconds();
211
+ for (const [key, entry] of this.entries) {
212
+ if (entry.expiresAt <= nowSeconds)
213
+ this.entries.delete(key);
214
+ }
215
+ this.writesSinceSweep = 0;
216
+ }
217
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * Both the rate limiter and the idempotency engine build a key by joining a
5
+ * few fields with a separator, and at least one field in each is caller data:
6
+ * a rate-limit key returned by a policy handler, an idempotency key posted
7
+ * with the request. A plain join lets two different field lists produce one
8
+ * string, so two callers land on one counter or one caller reads another's
9
+ * stored answer.
10
+ *
11
+ * Escaping the caller's use of the separator rather than rejecting or
12
+ * reserving it: the caller chose the character for its own reasons, and a
13
+ * limit that refuses a legal key is a bug of its own.
14
+ *
15
+ * The join is one-way. Nothing here reads a key back apart, because nothing
16
+ * needs to: a key is looked up, counted against or compared, never taken to
17
+ * pieces. The escaping is there so that distinct field lists cannot collide,
18
+ * not so that the fields can be recovered.
19
+ *
20
+ * LambderDdbCache's sort-key encoding looks similar and is deliberately the
21
+ * other thing: a reversible escape, because a cached entry's sort key is
22
+ * handed back to the caller by listSortKeys and therefore has to decode to
23
+ * exactly what was written. Two schemes, two jobs; neither should be reached
24
+ * for in the other's place.
25
+ *
26
+ * Written once because the two engines had drifted, one escaping and one not.
27
+ */
28
+ /**
29
+ * The fields joined into one key, each escaped, so no two distinct field
30
+ * lists can produce the same string.
31
+ */
32
+ export declare const joinKeyFields: (...fields: string[]) => string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * Both the rate limiter and the idempotency engine build a key by joining a
5
+ * few fields with a separator, and at least one field in each is caller data:
6
+ * a rate-limit key returned by a policy handler, an idempotency key posted
7
+ * with the request. A plain join lets two different field lists produce one
8
+ * string, so two callers land on one counter or one caller reads another's
9
+ * stored answer.
10
+ *
11
+ * Escaping the caller's use of the separator rather than rejecting or
12
+ * reserving it: the caller chose the character for its own reasons, and a
13
+ * limit that refuses a legal key is a bug of its own.
14
+ *
15
+ * The join is one-way. Nothing here reads a key back apart, because nothing
16
+ * needs to: a key is looked up, counted against or compared, never taken to
17
+ * pieces. The escaping is there so that distinct field lists cannot collide,
18
+ * not so that the fields can be recovered.
19
+ *
20
+ * LambderDdbCache's sort-key encoding looks similar and is deliberately the
21
+ * other thing: a reversible escape, because a cached entry's sort key is
22
+ * handed back to the caller by listSortKeys and therefore has to decode to
23
+ * exactly what was written. Two schemes, two jobs; neither should be reached
24
+ * for in the other's place.
25
+ *
26
+ * Written once because the two engines had drifted, one escaping and one not.
27
+ */
28
+ /** One field, with the separator and its own escape character escaped. */
29
+ const escapeKeyField = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
30
+ /**
31
+ * The fields joined into one key, each escaped, so no two distinct field
32
+ * lists can produce the same string.
33
+ */
34
+ export const joinKeyFields = (...fields) => fields.map(escapeKeyField).join("|");
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Node built-ins Lambder uses where they exist, and nothing where they do
3
+ * not: the same code runs on Lambda and in a browser, so every one of these
4
+ * is optional and every caller handles null.
5
+ */
6
+ export declare const getFS: () => Promise<typeof import('fs') | null>;
7
+ export declare const getPath: () => Promise<typeof import('path') | null>;
8
+ export declare const getZlib: () => Promise<typeof import('zlib') | null>;
9
+ export declare const getCrypto: () => Promise<typeof import('crypto') | null>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The Node built-ins Lambder uses where they exist, and nothing where they do
3
+ * not: the same code runs on Lambda and in a browser, so every one of these
4
+ * is optional and every caller handles null.
5
+ */
6
+ /**
7
+ * One of these modules, or null where the runtime has no such thing.
8
+ *
9
+ * A failed import is only half of "no such thing". The other half is a
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.
16
+ *
17
+ * The answer is memoized either way, absence included, so the probe runs once
18
+ * per module however often a request asks for it.
19
+ */
20
+ const loadNodeModule = (load, expect) => {
21
+ let pending = null;
22
+ return () => {
23
+ pending ??= (async () => {
24
+ try {
25
+ const nodeModule = await load();
26
+ return typeof nodeModule?.[expect] === "function" ? nodeModule : null;
27
+ }
28
+ catch {
29
+ // Silently fail - we're in a browser environment
30
+ return null;
31
+ }
32
+ })();
33
+ return pending;
34
+ };
35
+ };
36
+ export const getFS = loadNodeModule(() => import('fs'), "readFile");
37
+ export const getPath = loadNodeModule(() => import('path'), "join");
38
+ export const getZlib = loadNodeModule(() => import('zlib'), "gunzip");
39
+ export const getCrypto = loadNodeModule(() => import('crypto'), "createHash");
@@ -0,0 +1,17 @@
1
+ /**
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.
7
+ */
8
+ /** A safe integer of one or more; returns it so the check reads as an assignment. */
9
+ export declare const assertPositiveInteger: (value: unknown, name: string) => number;
10
+ /** A safe integer of zero or more; returns it so the check reads as an assignment. */
11
+ export declare const assertNonNegativeInteger: (value: unknown, name: string) => number;
12
+ /**
13
+ * A finite number at or above `minimum`, fractions included; returns it so
14
+ * the check reads as an assignment. For the options that scale something
15
+ * rather than count it, where 1.5 is a legitimate value.
16
+ */
17
+ export declare const assertNumberAtLeast: (value: unknown, minimum: number, name: string) => number;
@@ -0,0 +1,33 @@
1
+ /**
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.
7
+ */
8
+ const describe = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
9
+ /** A safe integer of one or more; returns it so the check reads as an assignment. */
10
+ export const assertPositiveInteger = (value, name) => {
11
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
12
+ throw new Error(`Lambder: ${name} must be a positive integer, got ${describe(value)}.`);
13
+ }
14
+ return value;
15
+ };
16
+ /** A safe integer of zero or more; returns it so the check reads as an assignment. */
17
+ export const assertNonNegativeInteger = (value, name) => {
18
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
19
+ throw new Error(`Lambder: ${name} must be a non-negative integer, got ${describe(value)}.`);
20
+ }
21
+ return value;
22
+ };
23
+ /**
24
+ * A finite number at or above `minimum`, fractions included; returns it so
25
+ * the check reads as an assignment. For the options that scale something
26
+ * rather than count it, where 1.5 is a legitimate value.
27
+ */
28
+ export const assertNumberAtLeast = (value, minimum, name) => {
29
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum) {
30
+ throw new Error(`Lambder: ${name} must be a number of ${minimum} or more, got ${describe(value)}.`);
31
+ }
32
+ return value;
33
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Marks an object as a response without anyone having to import the class to
3
+ * ask. Layers that must recognise one but must not depend on core at runtime
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.
10
+ */
11
+ export declare const LAMBDER_RESPONSE_BRAND: unique symbol;
12
+ /**
13
+ * Whether a value carries the response brand. Tested by brand rather than by
14
+ * `instanceof`, so a browser-side engine stays free of a runtime dependency
15
+ * on core and keeps working across realms and duplicate copies of the
16
+ * package.
17
+ */
18
+ export declare const isLambderResponseLike: (value: unknown) => value is {
19
+ readonly [LAMBDER_RESPONSE_BRAND]: true;
20
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Marks an object as a response without anyone having to import the class to
3
+ * ask. Layers that must recognise one but must not depend on core at runtime
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.
10
+ */
11
+ export const LAMBDER_RESPONSE_BRAND = Symbol.for("lambder.response");
12
+ /**
13
+ * Whether a value carries the response brand. Tested by brand rather than by
14
+ * `instanceof`, so a browser-side engine stays free of a runtime dependency
15
+ * on core and keeps working across realms and duplicate copies of the
16
+ * package.
17
+ */
18
+ export const isLambderResponseLike = (value) => typeof value === "object" && value !== null && value[LAMBDER_RESPONSE_BRAND] === true;
@@ -0,0 +1,17 @@
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).
7
+ */
8
+ /** Lowercase hex of a byte array, two characters per byte. */
9
+ export declare const bytesToHexString: (bytes: Uint8Array) => string;
10
+ /**
11
+ * globalThis.crypto where the runtime has it, else Node's webcrypto (a Node
12
+ * without the global). Resolved once per process; a runtime with neither
13
+ * throws, naming what is missing.
14
+ */
15
+ export declare const resolveWebCrypto: () => Promise<Crypto>;
16
+ /** The SHA-256 digest of `text` (UTF-8), as 64 lowercase hex characters. */
17
+ export declare const sha256HexOf: (text: string) => Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { getCrypto } from "./LambderNodeModules.js";
2
+ /**
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).
8
+ */
9
+ /** Lowercase hex of a byte array, two characters per byte. */
10
+ export const bytesToHexString = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
11
+ let webCryptoPromise;
12
+ /**
13
+ * globalThis.crypto where the runtime has it, else Node's webcrypto (a Node
14
+ * without the global). Resolved once per process; a runtime with neither
15
+ * throws, naming what is missing.
16
+ */
17
+ export const resolveWebCrypto = () => {
18
+ webCryptoPromise ??= (async () => {
19
+ if (typeof globalThis.crypto?.subtle?.digest === "function")
20
+ return globalThis.crypto;
21
+ const nodeCrypto = await getCrypto();
22
+ const webCrypto = nodeCrypto?.webcrypto;
23
+ if (webCrypto?.subtle)
24
+ return webCrypto;
25
+ throw new Error("Lambder needs WebCrypto (crypto.subtle) in this runtime. A browser provides it on a secure context (https or localhost); Node 20+ provides it as globalThis.crypto.");
26
+ })();
27
+ return webCryptoPromise;
28
+ };
29
+ /** The SHA-256 digest of `text` (UTF-8), as 64 lowercase hex characters. */
30
+ export const sha256HexOf = async (text) => {
31
+ const webCrypto = await resolveWebCrypto();
32
+ const digest = await webCrypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
33
+ return bytesToHexString(new Uint8Array(digest));
34
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The small type utilities more than one module needs.
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.
7
+ */
8
+ /**
9
+ * A value a caller may hand back either synchronously or as a promise. Every
10
+ * handler, hook and callback Lambder takes is declared over this: an app
11
+ * writes the synchronous form when it has nothing to await, and the framework
12
+ * awaits either.
13
+ */
14
+ export type MaybePromise<T> = T | Promise<T>;
15
+ /**
16
+ * A declaration map with AT LEAST ONE entry: the union, over every declarable
17
+ * name, of "this one required and the rest optional".
18
+ *
19
+ * An all-optional map is inhabited by `{}`, which would let `guards: {}`
20
+ * satisfy requireSessionApiGuards / requirePublicApiGuards at the type level
21
+ * while declaring no guard at all: the option is present, so the required-field
22
+ * check passes, and it normalizes to zero entries, so nothing runs. Requiring
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
25
+ * undefined param.
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.
30
+ */
31
+ export type LambderNonEmptyOptionMap<TMap> = {
32
+ [K in keyof TMap]-?: Required<Pick<TMap, K>> & Omit<TMap, K>;
33
+ }[keyof TMap];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The small type utilities more than one module needs.
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.
7
+ */
8
+ export {};