lambder 7.3.1 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +933 -3
- package/README.md +41 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +140 -75
- package/dist/core/Lambder.js +347 -227
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +1 -1
- package/dist/mock.js +2 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +21 -23
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +12 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -1,32 +1,82 @@
|
|
|
1
|
-
import { assertPartitionKeyFits, createDynamoClientLoader, isConditionalCheckFailure, } from "./LambderDdbSdk.js";
|
|
1
|
+
import { assertPartitionKeyFits, createDynamoClientLoader, isConditionalCheckFailure, isKeyRangeThrottle, } from "./LambderDdbSdk.js";
|
|
2
2
|
import { assertNumberAtLeast } from "../shared/util/LambderOptionChecks.js";
|
|
3
|
+
import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
|
|
4
|
+
import { joinKeyFields } from "../shared/util/joinKeyFields.js";
|
|
3
5
|
import { RATE_LIMIT_WINDOWS, } from "../shared/contracts/LambderRateLimiter.js";
|
|
6
|
+
/** How long a key refused during a throttle is told to wait: long enough to let the partition recover, short enough for a legitimate caller. */
|
|
7
|
+
const THROTTLED_RETRY_SECONDS = 5;
|
|
8
|
+
/**
|
|
9
|
+
* Most (tracker key, window) pairs the limiter remembers from throttles at
|
|
10
|
+
* once. Only a key seen during a key-range throttle is remembered, for
|
|
11
|
+
* THROTTLED_RETRY_SECONDS, so this is reached only by a flood spread over
|
|
12
|
+
* that many keys at once, where the ones closest to expiring go first.
|
|
13
|
+
*/
|
|
14
|
+
const THROTTLED_WINDOW_MEMORY_ENTRIES = 10_000;
|
|
15
|
+
/** The table key of one window's counter. */
|
|
16
|
+
const counterKeyOf = (partitionKey, window) => ({
|
|
17
|
+
pk: { S: partitionKey },
|
|
18
|
+
sk: { S: `${window.key}#${window.start}` },
|
|
19
|
+
});
|
|
20
|
+
/** The same counter's key in the limiter's memory of throttled windows, its fields kept apart as the table's two attributes keep them. */
|
|
21
|
+
const rememberedWindowKeyOf = (partitionKey, window) => joinKeyFields(partitionKey, window.key, String(window.start));
|
|
4
22
|
/**
|
|
5
23
|
* Fixed-window rate limiter backed by DynamoDB.
|
|
6
24
|
*
|
|
7
|
-
* Each window is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* attribute for DynamoDB TTL.
|
|
25
|
+
* Each window is one item counted with a conditional `ADD`, so the increment
|
|
26
|
+
* and the limit check are one atomic request. Windows are evaluated smallest
|
|
27
|
+
* first and evaluation stops at the first exceeded one, which keeps blocked
|
|
28
|
+
* requests cheap and spares the larger counters. Attempts count, not
|
|
29
|
+
* successes: a counter checked before the refusing one keeps its increment,
|
|
30
|
+
* since a compensating decrement would give up the conditional-ADD
|
|
31
|
+
* atomicity. Items carry an `expiresAt` attribute for DynamoDB TTL.
|
|
15
32
|
*
|
|
16
33
|
* The tracker key is caller data (an address, a session key, whatever a
|
|
17
34
|
* policy handler returned), so a key whose partition key would pass
|
|
18
|
-
* DynamoDB's 2048-byte limit is refused here, before any window is counted
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
35
|
+
* DynamoDB's 2048-byte limit is refused here, before any window is counted.
|
|
36
|
+
* At the table it would come back as a ValidationException, which is not a
|
|
37
|
+
* conditional-check failure: it would escape as a store error, and a caller
|
|
38
|
+
* failing open on it would count nothing, leaving the limit silently off.
|
|
39
|
+
* Lambder's own engine folds an over-long key into a digest first, so a key
|
|
40
|
+
* that gets here came from a direct caller.
|
|
41
|
+
*
|
|
42
|
+
* A DynamoDB error propagates: a limiter cannot say whether the caller is
|
|
43
|
+
* over its limit when it cannot reach the table, and whether an unanswerable
|
|
44
|
+
* limit lets the request through is the application's call, made once for
|
|
45
|
+
* every limiter at `rateLimits.failOpen`.
|
|
46
|
+
*
|
|
47
|
+
* A throttle on the key's range can be an answer instead. DynamoDB
|
|
48
|
+
* throttles a partition's writes at roughly a thousand a second, and the SDK
|
|
49
|
+
* retries first, so a throttle whose reason is KeyRangeThroughputExceeded
|
|
50
|
+
* means the partition holding this counter is flooded. A partition holds a
|
|
51
|
+
* range of keys, though, not one: a flood on one address, or a session or
|
|
52
|
+
* cache spike on a shared table, throttles every counter on the same
|
|
53
|
+
* partition. The key's own counts tell the flood from its neighbours: the
|
|
54
|
+
* throttled window's and every capped window's after it, the ones this
|
|
55
|
+
* attempt has not been counted against yet, each read with a consistent
|
|
56
|
+
* GetItem, in parallel (reads have their own throughput, which the throttled
|
|
57
|
+
* writes leave alone). A key at or over the limit of any of them is the
|
|
58
|
+
* flood: it is refused, since passed on as a failure `failOpen` would wave
|
|
59
|
+
* the flood through unmetered, and the Retry-After is a few seconds, the
|
|
60
|
+
* time the partition takes to recover, rather than the window's reset. A key
|
|
61
|
+
* over its daily cap whose per-minute counter has just started again is the
|
|
62
|
+
* flood as much as one over its per-minute cap. Under every limit, or when a
|
|
63
|
+
* read fails too, the key is a neighbour: the throttle propagates like any
|
|
64
|
+
* other store failure and `failOpen` decides, as it does for a throttle of
|
|
65
|
+
* the table or the account (its provisioned capacity, an on-demand maximum,
|
|
66
|
+
* the account's quota), so no caller under its limit is refused because the
|
|
67
|
+
* table is busy with somebody else.
|
|
24
68
|
*
|
|
25
|
-
* A
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
69
|
+
* A flood repeats, and each repeat would cost the partition another
|
|
70
|
+
* throttled write and another consistent read, until the reads throttle too
|
|
71
|
+
* and the flood fails open. So the process remembers, for
|
|
72
|
+
* THROTTLED_RETRY_SECONDS, each window it read at its limit, and refuses the
|
|
73
|
+
* key's next attempts from memory without touching the table, which also
|
|
74
|
+
* lets the partition recover for the neighbours. A count only rises within
|
|
75
|
+
* its window, so the table would answer the same. A window whose read was
|
|
76
|
+
* throttled as well is remembered too, with the throttle it was answered
|
|
77
|
+
* with: a next attempt whose write is throttled there again skips the read
|
|
78
|
+
* and throws that same throttle, which `rateLimits.failOpen` logs once
|
|
79
|
+
* rather than once per request.
|
|
30
80
|
*
|
|
31
81
|
* Table shape: string hash key `pk`, string range key `sk`, TTL on `expiresAt`.
|
|
32
82
|
* Items are prefixed `RL#` by default, so the table can be shared with
|
|
@@ -40,6 +90,8 @@ export class LambderDdbRateLimiter {
|
|
|
40
90
|
ready;
|
|
41
91
|
ttlWindowMultiplier;
|
|
42
92
|
now;
|
|
93
|
+
/** The windows seen during key-range throttles, by (partition key, window, window start); see the class doc. */
|
|
94
|
+
throttledWindows;
|
|
43
95
|
constructor(options) {
|
|
44
96
|
if (!options.tableName.trim())
|
|
45
97
|
throw new Error("tableName is required");
|
|
@@ -47,8 +99,13 @@ export class LambderDdbRateLimiter {
|
|
|
47
99
|
this.keyPrefix = options.keyPrefix ?? "RL";
|
|
48
100
|
this.ttlWindowMultiplier = assertNumberAtLeast(options.ttlWindowMultiplier ?? 2, 1, "ttlWindowMultiplier");
|
|
49
101
|
this.now = options.now ?? (() => Date.now());
|
|
102
|
+
this.throttledWindows = new LambderExpiringMap({ now: this.now, maxEntries: THROTTLED_WINDOW_MEMORY_ENTRIES });
|
|
50
103
|
this.ready = createDynamoClientLoader({ user: "LambderDdbRateLimiter", region: options.region, client: options.client });
|
|
51
104
|
}
|
|
105
|
+
/** The clock the windows are computed against, for the engine's Retry-After. */
|
|
106
|
+
clockMilliseconds() {
|
|
107
|
+
return this.now();
|
|
108
|
+
}
|
|
52
109
|
/**
|
|
53
110
|
* Increment every configured window for `trackerKey` (IP, session, user id, ...)
|
|
54
111
|
* and report whether any of them is over its limit, with the window's
|
|
@@ -61,14 +118,30 @@ export class LambderDdbRateLimiter {
|
|
|
61
118
|
// refusing it here is the difference between one clear error and a
|
|
62
119
|
// policy that counts nothing while reporting nothing.
|
|
63
120
|
const partitionKey = this.partitionKeyFor(trackerKey);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
121
|
+
const windows = RATE_LIMIT_WINDOWS
|
|
122
|
+
.filter(({ key }) => policy[key])
|
|
123
|
+
.map(({ key, seconds }) => ({ key, seconds, limit: policy[key], start: Math.floor(nowSeconds / seconds) * seconds }));
|
|
124
|
+
// A window this process read at its limit during a throttle refuses
|
|
125
|
+
// without touching the table (see the class doc).
|
|
126
|
+
const rememberedAtLimit = windows.find((window) => {
|
|
127
|
+
const remembered = this.throttledWindows.get(rememberedWindowKeyOf(partitionKey, window));
|
|
128
|
+
return remembered !== undefined && "count" in remembered && remembered.count >= window.limit;
|
|
129
|
+
});
|
|
130
|
+
if (rememberedAtLimit)
|
|
131
|
+
return { window: rememberedAtLimit.key, limit: rememberedAtLimit.limit, resetAt: nowSeconds + THROTTLED_RETRY_SECONDS };
|
|
132
|
+
for (const [index, window] of windows.entries()) {
|
|
133
|
+
try {
|
|
134
|
+
if (await this.incrementWindow(partitionKey, window, nowSeconds))
|
|
135
|
+
return { window: window.key, limit: window.limit, resetAt: window.start + window.seconds };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// A key-range throttle may be the limiter's own answer (see
|
|
139
|
+
// the class doc); anything else is the table failing, which
|
|
140
|
+
// only the caller can decide what to do about.
|
|
141
|
+
if (!isKeyRangeThrottle(error))
|
|
142
|
+
throw error;
|
|
143
|
+
return await this.answerKeyRangeThrottle(partitionKey, windows.slice(index), nowSeconds, error);
|
|
144
|
+
}
|
|
72
145
|
}
|
|
73
146
|
return false;
|
|
74
147
|
}
|
|
@@ -81,22 +154,23 @@ export class LambderDdbRateLimiter {
|
|
|
81
154
|
remedy: "Shorten the key the policy hands the limiter.",
|
|
82
155
|
});
|
|
83
156
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Increments one window counter. True when the limit was already reached
|
|
159
|
+
* (the refused condition is the limiter's own answer); any other failure
|
|
160
|
+
* throws.
|
|
161
|
+
*/
|
|
162
|
+
async incrementWindow(partitionKey, window, nowSeconds) {
|
|
163
|
+
const expiresAt = nowSeconds + Math.ceil(window.seconds * this.ttlWindowMultiplier);
|
|
87
164
|
const input = {
|
|
88
165
|
TableName: this.tableName,
|
|
89
|
-
Key:
|
|
90
|
-
pk: { S: partitionKey },
|
|
91
|
-
sk: { S: `${sortKeyPrefix}#${windowStart}` },
|
|
92
|
-
},
|
|
166
|
+
Key: counterKeyOf(partitionKey, window),
|
|
93
167
|
UpdateExpression: "ADD #count :one SET #expiresAt = if_not_exists(#expiresAt, :expiresAt)",
|
|
94
168
|
ConditionExpression: "attribute_not_exists(#count) OR #count < :limit",
|
|
95
169
|
ExpressionAttributeNames: { "#count": "count", "#expiresAt": "expiresAt" },
|
|
96
170
|
ExpressionAttributeValues: {
|
|
97
171
|
":one": { N: "1" },
|
|
98
172
|
":expiresAt": { N: String(expiresAt) },
|
|
99
|
-
":limit": { N: String(limit) },
|
|
173
|
+
":limit": { N: String(window.limit) },
|
|
100
174
|
},
|
|
101
175
|
};
|
|
102
176
|
try {
|
|
@@ -105,13 +179,51 @@ export class LambderDdbRateLimiter {
|
|
|
105
179
|
return false;
|
|
106
180
|
}
|
|
107
181
|
catch (error) {
|
|
108
|
-
// The refused condition is the limiter's own answer; anything else
|
|
109
|
-
// is the table being unreachable, which only the caller can decide
|
|
110
|
-
// what to do about.
|
|
111
182
|
if (isConditionalCheckFailure(error))
|
|
112
183
|
return true;
|
|
113
184
|
throw error;
|
|
114
185
|
}
|
|
115
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The answer to a key-range throttle on the first of `uncounted`: that
|
|
189
|
+
* window and every capped one after it, the windows this attempt has not
|
|
190
|
+
* been counted against. The windows before it counted this attempt and
|
|
191
|
+
* allowed it, so reading them could only turn the attempt that filled
|
|
192
|
+
* one into a refusal.
|
|
193
|
+
*
|
|
194
|
+
* Each count is read strongly consistent: the count that decides is the
|
|
195
|
+
* one the throttled writes were racing to raise, and a replica lagging
|
|
196
|
+
* behind them could read a key that has just reached its limit as under
|
|
197
|
+
* it. Any window at or over its limit refuses, and is remembered (see the
|
|
198
|
+
* class doc). Otherwise the throttle is thrown on, and when a read was
|
|
199
|
+
* throttled too, the throttled window is remembered with it, so the
|
|
200
|
+
* key's next attempts skip the read and throw that same throttle.
|
|
201
|
+
*/
|
|
202
|
+
async answerKeyRangeThrottle(partitionKey, uncounted, nowSeconds, throttle) {
|
|
203
|
+
const throttledWindowKey = rememberedWindowKeyOf(partitionKey, uncounted[0]);
|
|
204
|
+
const remembered = this.throttledWindows.get(throttledWindowKey);
|
|
205
|
+
if (remembered !== undefined && "unreadable" in remembered)
|
|
206
|
+
throw remembered.unreadable;
|
|
207
|
+
const { client, sdk } = await this.ready();
|
|
208
|
+
const reads = await Promise.allSettled(uncounted.map(async (window) => {
|
|
209
|
+
const { Item } = await client.send(new sdk.GetItemCommand({ TableName: this.tableName, Key: counterKeyOf(partitionKey, window), ConsistentRead: true }));
|
|
210
|
+
return Number(Item?.count?.N ?? 0);
|
|
211
|
+
}));
|
|
212
|
+
const rememberUntil = nowSeconds + THROTTLED_RETRY_SECONDS;
|
|
213
|
+
let refusing;
|
|
214
|
+
for (const [index, read] of reads.entries()) {
|
|
215
|
+
const window = uncounted[index];
|
|
216
|
+
if (read.status === "fulfilled" && read.value >= window.limit) {
|
|
217
|
+
this.throttledWindows.set(rememberedWindowKeyOf(partitionKey, window), { count: read.value }, rememberUntil);
|
|
218
|
+
refusing ??= window;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (refusing)
|
|
222
|
+
return { window: refusing.key, limit: refusing.limit, resetAt: rememberUntil };
|
|
223
|
+
if (reads.some((read) => read.status === "rejected" && isKeyRangeThrottle(read.reason))) {
|
|
224
|
+
this.throttledWindows.set(throttledWindowKey, { unreadable: throttle }, rememberUntil);
|
|
225
|
+
}
|
|
226
|
+
throw throttle;
|
|
227
|
+
}
|
|
116
228
|
}
|
|
117
229
|
export default LambderDdbRateLimiter;
|
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
* The DynamoDB SDK, loaded on first use.
|
|
3
3
|
*
|
|
4
4
|
* `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` are optional peer
|
|
5
|
-
* dependencies
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* install hint rather than failing the import of lambder itself.
|
|
5
|
+
* dependencies: an app that uses no DynamoDB store should neither install
|
|
6
|
+
* them nor pay for loading them, and a module-level import would cost every
|
|
7
|
+
* cold start and make every bundled Lambder app reference both packages. So
|
|
8
|
+
* the session manager and the stores import their types only and take the
|
|
9
|
+
* classes from here the first time they touch the table, as
|
|
10
|
+
* LambderS3FileSource and LambderInvokeCaller do. One loader per package,
|
|
11
|
+
* memoized for the container's life; a missing package fails that first call
|
|
12
|
+
* with the install hint rather than failing the import of lambder itself.
|
|
14
13
|
*
|
|
15
|
-
* The
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* limit that the caller data these stores build keys from can pass.
|
|
14
|
+
* The facts about DynamoDB itself that every store must agree on live here
|
|
15
|
+
* too: a conditional write's refusal is an answer rather than a failure, and
|
|
16
|
+
* a partition key has a limit that keys built from caller data can pass.
|
|
19
17
|
*/
|
|
20
18
|
import type { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
21
19
|
import type { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
@@ -28,19 +26,16 @@ export type LambderDynamoClientReady = {
|
|
|
28
26
|
};
|
|
29
27
|
/**
|
|
30
28
|
* The "ready" step each DynamoDB-backed store runs before its first table
|
|
31
|
-
* call: load the SDK, then take the client the app supplied or
|
|
32
|
-
*
|
|
33
|
-
* store that ran before the package was
|
|
34
|
-
* caching the install hint
|
|
29
|
+
* call: load the SDK, then take the client the app supplied or the shared
|
|
30
|
+
* default one for the store's region. Memoized for the store's life, except
|
|
31
|
+
* that a FAILED load is not, so a store that ran before the package was
|
|
32
|
+
* installed asks again rather than caching the install hint forever.
|
|
35
33
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
41
|
-
* `region` absent means the SDK's default chain (`AWS_REGION`, the Lambda
|
|
42
|
-
* environment, the shared config file), which is what a store should leave
|
|
43
|
-
* alone unless the caller names one.
|
|
34
|
+
* One implementation for every store, so they cannot drift apart on the
|
|
35
|
+
* region. `region` absent means the SDK's default chain (`AWS_REGION`, the
|
|
36
|
+
* Lambda environment, the shared config file); a store leaves it alone
|
|
37
|
+
* unless the caller names one, since pinning one (say `us-east-1`) would
|
|
38
|
+
* ignore the deployment's own region.
|
|
44
39
|
*/
|
|
45
40
|
export declare const createDynamoClientLoader: (options: {
|
|
46
41
|
user: string;
|
|
@@ -71,6 +66,23 @@ export declare const createDynamoDocumentClientLoader: (options: {
|
|
|
71
66
|
* and the reason they must not catch any other.
|
|
72
67
|
*/
|
|
73
68
|
export declare const isConditionalCheckFailure: (error: unknown) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether DynamoDB refused a request, after the SDK's own retries, because
|
|
71
|
+
* of the rate at one key range, rather than the table's or the account's.
|
|
72
|
+
* A key range is a partition, which holds many keys, so the throttle falls
|
|
73
|
+
* on the key asked about and on its neighbours alike: whether this key is
|
|
74
|
+
* the one flooding it is the caller's to find out.
|
|
75
|
+
*
|
|
76
|
+
* Every throttle names why in its ThrottlingReasons (throttlingReasons on a
|
|
77
|
+
* ThrottlingException), as `<Table|Index><Read|Write><LimitType>`; only
|
|
78
|
+
* KeyRangeThroughputExceeded is about one partition. The other limit types
|
|
79
|
+
* (ProvisionedThroughputExceeded, AccountLimitExceeded,
|
|
80
|
+
* MaxOnDemandThroughputExceeded) are the table or the account running out,
|
|
81
|
+
* which says nothing about any one partition. An SDK older than the reasons
|
|
82
|
+
* (`@aws-sdk/client-dynamodb` before 3.868.0, the peer dependency's
|
|
83
|
+
* minimum), or a local DynamoDB, names none, and reads as not a key range's.
|
|
84
|
+
*/
|
|
85
|
+
export declare const isKeyRangeThrottle: (error: unknown) => boolean;
|
|
74
86
|
/**
|
|
75
87
|
* DynamoDB's own limit on a partition key, which every store's
|
|
76
88
|
* `<keyPrefix>#<caller data>` key has to fit inside.
|
|
@@ -78,14 +90,14 @@ export declare const isConditionalCheckFailure: (error: unknown) => boolean;
|
|
|
78
90
|
export declare const MAX_PARTITION_KEY_BYTES = 2048;
|
|
79
91
|
/**
|
|
80
92
|
* The partition key, or a refusal naming the byte count. Every store here
|
|
81
|
-
* builds its key
|
|
82
|
-
*
|
|
93
|
+
* builds its key from caller data (an idempotency key and the identity it is
|
|
94
|
+
* scoped by, a rate-limit key a policy handler returned), so the length is
|
|
83
95
|
* the caller's to reach and this is the one place that measures it.
|
|
84
96
|
*
|
|
85
|
-
* Checked
|
|
86
|
-
* ValidationException
|
|
87
|
-
*
|
|
88
|
-
* fail-open setting,
|
|
97
|
+
* Checked here because DynamoDB answers an over-long key with a
|
|
98
|
+
* ValidationException, not a ConditionalCheckFailedException: it would
|
|
99
|
+
* escape as a store error that reads as "the table is broken" and, under a
|
|
100
|
+
* fail-open setting, be swallowed into no protection at all. The message
|
|
89
101
|
* carries the count and never the key, because it reaches a log.
|
|
90
102
|
*/
|
|
91
103
|
export declare const assertPartitionKeyFits: (options: {
|
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
* The DynamoDB SDK, loaded on first use.
|
|
3
3
|
*
|
|
4
4
|
* `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` are optional peer
|
|
5
|
-
* dependencies
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* install hint rather than failing the import of lambder itself.
|
|
5
|
+
* dependencies: an app that uses no DynamoDB store should neither install
|
|
6
|
+
* them nor pay for loading them, and a module-level import would cost every
|
|
7
|
+
* cold start and make every bundled Lambder app reference both packages. So
|
|
8
|
+
* the session manager and the stores import their types only and take the
|
|
9
|
+
* classes from here the first time they touch the table, as
|
|
10
|
+
* LambderS3FileSource and LambderInvokeCaller do. One loader per package,
|
|
11
|
+
* memoized for the container's life; a missing package fails that first call
|
|
12
|
+
* with the install hint rather than failing the import of lambder itself.
|
|
14
13
|
*
|
|
15
|
-
* The
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* limit that the caller data these stores build keys from can pass.
|
|
14
|
+
* The facts about DynamoDB itself that every store must agree on live here
|
|
15
|
+
* too: a conditional write's refusal is an answer rather than a failure, and
|
|
16
|
+
* a partition key has a limit that keys built from caller data can pass.
|
|
19
17
|
*/
|
|
20
18
|
let clientSdk;
|
|
21
19
|
let documentSdk;
|
|
@@ -34,27 +32,42 @@ const loadDynamoDocumentSdk = (user) => {
|
|
|
34
32
|
documentSdk ??= withInstallHint(import("@aws-sdk/lib-dynamodb"), "@aws-sdk/lib-dynamodb", user, () => { documentSdk = undefined; });
|
|
35
33
|
return documentSdk;
|
|
36
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The default clients, one per region ("" for the SDK's default chain),
|
|
37
|
+
* shared by every store not given a client of its own. A client is a
|
|
38
|
+
* connection pool and a credential chain: sessions, rate limits,
|
|
39
|
+
* idempotency and the cache building one each would mean four pools and, on
|
|
40
|
+
* a cold container, four TLS handshakes and credential lookups inside the
|
|
41
|
+
* first request.
|
|
42
|
+
*/
|
|
43
|
+
const defaultClients = new Map();
|
|
44
|
+
const defaultDocumentClients = new Map();
|
|
45
|
+
const defaultClientFor = (sdk, region) => {
|
|
46
|
+
let client = defaultClients.get(region ?? "");
|
|
47
|
+
if (!client) {
|
|
48
|
+
client = new sdk.DynamoDBClient(region ? { region } : {});
|
|
49
|
+
defaultClients.set(region ?? "", client);
|
|
50
|
+
}
|
|
51
|
+
return client;
|
|
52
|
+
};
|
|
37
53
|
/**
|
|
38
54
|
* The "ready" step each DynamoDB-backed store runs before its first table
|
|
39
|
-
* call: load the SDK, then take the client the app supplied or
|
|
40
|
-
*
|
|
41
|
-
* store that ran before the package was
|
|
42
|
-
* caching the install hint
|
|
43
|
-
*
|
|
44
|
-
* Written once because four stores had written it for themselves and had
|
|
45
|
-
* already drifted apart on the region: three passed the SDK's default chain
|
|
46
|
-
* through and the cache pinned `us-east-1`, so one store out of four ignored
|
|
47
|
-
* the deployment's own region.
|
|
55
|
+
* call: load the SDK, then take the client the app supplied or the shared
|
|
56
|
+
* default one for the store's region. Memoized for the store's life, except
|
|
57
|
+
* that a FAILED load is not, so a store that ran before the package was
|
|
58
|
+
* installed asks again rather than caching the install hint forever.
|
|
48
59
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
60
|
+
* One implementation for every store, so they cannot drift apart on the
|
|
61
|
+
* region. `region` absent means the SDK's default chain (`AWS_REGION`, the
|
|
62
|
+
* Lambda environment, the shared config file); a store leaves it alone
|
|
63
|
+
* unless the caller names one, since pinning one (say `us-east-1`) would
|
|
64
|
+
* ignore the deployment's own region.
|
|
52
65
|
*/
|
|
53
66
|
export const createDynamoClientLoader = (options) => {
|
|
54
67
|
let readyPromise;
|
|
55
68
|
return () => {
|
|
56
69
|
readyPromise ??= loadDynamoClientSdk(options.user)
|
|
57
|
-
.then((sdk) => ({ sdk, client: options.client ??
|
|
70
|
+
.then((sdk) => ({ sdk, client: options.client ?? defaultClientFor(sdk, options.region) }))
|
|
58
71
|
.catch((error) => { readyPromise = undefined; throw error; });
|
|
59
72
|
return readyPromise;
|
|
60
73
|
};
|
|
@@ -71,7 +84,14 @@ export const createDynamoDocumentClientLoader = (options) => {
|
|
|
71
84
|
if (options.client)
|
|
72
85
|
return { sdk: await loadDynamoDocumentSdk(options.user), client: options.client };
|
|
73
86
|
const [clientSdk, sdk] = await Promise.all([loadDynamoClientSdk(options.user), loadDynamoDocumentSdk(options.user)]);
|
|
74
|
-
|
|
87
|
+
// Over the same shared client the item-level stores use, so the
|
|
88
|
+
// session store's calls go through that one connection pool too.
|
|
89
|
+
let client = defaultDocumentClients.get(options.region ?? "");
|
|
90
|
+
if (!client) {
|
|
91
|
+
client = sdk.DynamoDBDocumentClient.from(defaultClientFor(clientSdk, options.region));
|
|
92
|
+
defaultDocumentClients.set(options.region ?? "", client);
|
|
93
|
+
}
|
|
94
|
+
return { sdk, client };
|
|
75
95
|
};
|
|
76
96
|
return () => {
|
|
77
97
|
readyPromise ??= build().catch((error) => { readyPromise = undefined; throw error; });
|
|
@@ -86,6 +106,32 @@ export const createDynamoDocumentClientLoader = (options) => {
|
|
|
86
106
|
* and the reason they must not catch any other.
|
|
87
107
|
*/
|
|
88
108
|
export const isConditionalCheckFailure = (error) => !!error && typeof error === "object" && "name" in error && error.name === "ConditionalCheckFailedException";
|
|
109
|
+
/** DynamoDB's names for "too many requests": a hot partition, the table's or account's throughput, the request rate. */
|
|
110
|
+
const THROTTLE_ERROR_NAMES = ["ProvisionedThroughputExceededException", "ThrottlingException", "RequestLimitExceeded"];
|
|
111
|
+
/**
|
|
112
|
+
* Whether DynamoDB refused a request, after the SDK's own retries, because
|
|
113
|
+
* of the rate at one key range, rather than the table's or the account's.
|
|
114
|
+
* A key range is a partition, which holds many keys, so the throttle falls
|
|
115
|
+
* on the key asked about and on its neighbours alike: whether this key is
|
|
116
|
+
* the one flooding it is the caller's to find out.
|
|
117
|
+
*
|
|
118
|
+
* Every throttle names why in its ThrottlingReasons (throttlingReasons on a
|
|
119
|
+
* ThrottlingException), as `<Table|Index><Read|Write><LimitType>`; only
|
|
120
|
+
* KeyRangeThroughputExceeded is about one partition. The other limit types
|
|
121
|
+
* (ProvisionedThroughputExceeded, AccountLimitExceeded,
|
|
122
|
+
* MaxOnDemandThroughputExceeded) are the table or the account running out,
|
|
123
|
+
* which says nothing about any one partition. An SDK older than the reasons
|
|
124
|
+
* (`@aws-sdk/client-dynamodb` before 3.868.0, the peer dependency's
|
|
125
|
+
* minimum), or a local DynamoDB, names none, and reads as not a key range's.
|
|
126
|
+
*/
|
|
127
|
+
export const isKeyRangeThrottle = (error) => {
|
|
128
|
+
if (!error || typeof error !== "object" || !("name" in error) || !THROTTLE_ERROR_NAMES.includes(String(error.name)))
|
|
129
|
+
return false;
|
|
130
|
+
const { ThrottlingReasons, throttlingReasons } = error;
|
|
131
|
+
const reasons = ThrottlingReasons ?? throttlingReasons;
|
|
132
|
+
return Array.isArray(reasons)
|
|
133
|
+
&& reasons.some((reason) => typeof reason?.reason === "string" && reason.reason.endsWith("KeyRangeThroughputExceeded"));
|
|
134
|
+
};
|
|
89
135
|
/**
|
|
90
136
|
* DynamoDB's own limit on a partition key, which every store's
|
|
91
137
|
* `<keyPrefix>#<caller data>` key has to fit inside.
|
|
@@ -93,14 +139,14 @@ export const isConditionalCheckFailure = (error) => !!error && typeof error ===
|
|
|
93
139
|
export const MAX_PARTITION_KEY_BYTES = 2048;
|
|
94
140
|
/**
|
|
95
141
|
* The partition key, or a refusal naming the byte count. Every store here
|
|
96
|
-
* builds its key
|
|
97
|
-
*
|
|
142
|
+
* builds its key from caller data (an idempotency key and the identity it is
|
|
143
|
+
* scoped by, a rate-limit key a policy handler returned), so the length is
|
|
98
144
|
* the caller's to reach and this is the one place that measures it.
|
|
99
145
|
*
|
|
100
|
-
* Checked
|
|
101
|
-
* ValidationException
|
|
102
|
-
*
|
|
103
|
-
* fail-open setting,
|
|
146
|
+
* Checked here because DynamoDB answers an over-long key with a
|
|
147
|
+
* ValidationException, not a ConditionalCheckFailedException: it would
|
|
148
|
+
* escape as a store error that reads as "the table is broken" and, under a
|
|
149
|
+
* fail-open setting, be swallowed into no protection at all. The message
|
|
104
150
|
* carries the count and never the key, because it reaches a log.
|
|
105
151
|
*/
|
|
106
152
|
export const assertPartitionKeyFits = (options) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
2
2
|
import { type LambderCompressionOption } from "../shared/wire/LambderCompressionOption.js";
|
|
3
|
-
import type { LambderSessionRecord, LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
3
|
+
import type { LambderSessionChanges, LambderSessionRecord, LambderSessionStore, LambderSessionUpdateResult } from "../shared/contracts/LambderSessionStore.js";
|
|
4
4
|
export type LambderDdbSessionStoreOptions = {
|
|
5
5
|
tableName: string;
|
|
6
6
|
/** Region the client is created for on first use; the SDK's default chain otherwise. */
|
|
@@ -28,6 +28,10 @@ export type LambderDdbSessionStoreOptions = {
|
|
|
28
28
|
* Table shape: a string hash key (the salted sessionKey hash, every session
|
|
29
29
|
* of one subject shares it) and a string range key (the bearer secret's
|
|
30
30
|
* hash), plus a TTL on `expiresAt` to let DynamoDB sweep expired sessions.
|
|
31
|
+
*
|
|
32
|
+
* Every write is conditional: a create on the item not existing, an update
|
|
33
|
+
* on it existing, so no write already in flight can bring back a session a
|
|
34
|
+
* logout deleted.
|
|
31
35
|
*/
|
|
32
36
|
export declare class LambderDdbSessionStore<SessionData = unknown> implements LambderSessionStore<SessionData> {
|
|
33
37
|
/** DynamoDB keeps records after this process is gone. */
|
|
@@ -40,26 +44,35 @@ export declare class LambderDdbSessionStore<SessionData = unknown> implements La
|
|
|
40
44
|
private readonly ready;
|
|
41
45
|
constructor(options: LambderDdbSessionStoreOptions);
|
|
42
46
|
private keyOf;
|
|
47
|
+
/**
|
|
48
|
+
* session.data as the attributes that hold it: `dataBr` and `dataBytes`
|
|
49
|
+
* when compressed, a plain `data` otherwise. Either way it goes through
|
|
50
|
+
* its JSON first, so a plain record holds exactly what a compressed one
|
|
51
|
+
* restores to: an `undefined` inside the data is dropped rather than
|
|
52
|
+
* handed to the document client, which refuses one and would fail the
|
|
53
|
+
* write (a login answering 500) only when compression is off.
|
|
54
|
+
*/
|
|
55
|
+
private dataAttributes;
|
|
43
56
|
/** The item for a record: the two hashes under the table's key names, the data plain or compressed. */
|
|
44
57
|
private toItem;
|
|
45
58
|
/**
|
|
46
59
|
* The record for an item. A compressed record decodes back into `data`;
|
|
47
|
-
* one whose data cannot be decoded is
|
|
48
|
-
*
|
|
60
|
+
* one whose data cannot be decoded is malformed and reads as no session,
|
|
61
|
+
* like a record missing its csrfTokenHash.
|
|
49
62
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* clear, on every request, until the TTL retires it. Ending it lets them
|
|
57
|
-
* log in again.
|
|
63
|
+
* This is where read failures and malformed records separate. A read
|
|
64
|
+
* failure is infrastructure and must surface as a 500: signing somebody
|
|
65
|
+
* out over a transient DynamoDB error is worse than an error page. A
|
|
66
|
+
* record that will not decode will not decode on the next request either,
|
|
67
|
+
* so a 500 there would be a session the visitor can neither use nor clear
|
|
68
|
+
* until the TTL retires it. Ending it lets them log in again.
|
|
58
69
|
*/
|
|
59
70
|
private fromItem;
|
|
60
71
|
get(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
create(record: LambderSessionRecord<SessionData>): Promise<void>;
|
|
73
|
+
update(sessionKeyHash: string, secretHash: string, changes: LambderSessionChanges<SessionData>, condition?: {
|
|
74
|
+
dataVersion: number;
|
|
75
|
+
}): Promise<LambderSessionUpdateResult>;
|
|
76
|
+
delete(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
|
|
63
77
|
listSecretHashes(sessionKeyHash: string): Promise<string[]>;
|
|
64
|
-
markDataExpired(sessionKeyHash: string, secretHash: string, at: number): Promise<void>;
|
|
65
78
|
}
|