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.
- package/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +26 -24
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /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 {};
|