lambder 12.0.1 → 14.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 +119 -0
- package/README.md +7 -2
- package/dist/api/LambderApiAnswer.d.ts +7 -0
- package/dist/api/LambderApiEnvelope.js +4 -1
- package/dist/api/LambderApiRefusals.d.ts +22 -0
- package/dist/api/LambderApiRefusals.js +13 -0
- package/dist/build/generateApiFiles.d.ts +60 -0
- package/dist/build/generateApiFiles.js +85 -0
- package/dist/build/generatedTables.d.ts +1 -1
- package/dist/build/writeApiGuardParams.d.ts +11 -10
- package/dist/build/writeApiGuardParams.js +135 -31
- package/dist/build.d.ts +4 -1
- package/dist/build.js +3 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/core/Lambder.d.ts +32 -8
- package/dist/core/Lambder.js +135 -17
- package/dist/core/LambderCallSummary.d.ts +43 -0
- package/dist/core/LambderCallSummary.js +10 -0
- package/dist/core/LambderContext.d.ts +14 -1
- package/dist/core/LambderContext.js +19 -4
- package/dist/core/LambderCreateOptions.d.ts +50 -25
- package/dist/core/LambderCreateOptions.js +37 -18
- package/dist/core/LambderInvocationScope.d.ts +25 -0
- package/dist/core/LambderInvocationScope.js +21 -0
- package/dist/core/LambderResponse.d.ts +11 -0
- package/dist/core/LambderResponse.js +12 -0
- package/dist/core/LambderResponseBuilder.js +5 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -2
- package/dist/invoke/LambderLambdaEvent.d.ts +2 -0
- package/dist/invoke/LambderLambdaEvent.js +8 -2
- package/dist/mock/LambderMockApp.d.ts +8 -8
- package/dist/mock/LambderMockApp.js +14 -10
- package/dist/mock/LambderMockCreateOptions.d.ts +2 -3
- package/dist/session/LambderSessionManager.d.ts +46 -3
- package/dist/session/LambderSessionManager.js +62 -11
- package/dist/shared/LambderI18n.js +49 -4
- package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
- package/dist/shared/contracts/LambderRateLimiter.js +2 -2
- package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
- package/dist/shared/util/LambderInvocationScope.js +5 -0
- package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
- package/dist/shared/util/LambderNamedMaps.js +19 -0
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +24 -0
- package/dist/shared/wire/LambderApiOptionEntries.js +13 -0
- package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
- package/dist/shared/wire/LambderCallOutcome.js +47 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
- package/dist/shared/wire/LambderInvokeApiId.js +10 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
- package/dist/stores/LambderDdbRateLimiter.js +64 -45
- package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
- package/dist/stores/LambderMemoryRateLimiter.js +20 -15
- package/dist/testing/LambderTestApp.d.ts +13 -4
- package/dist/testing/LambderTestApp.js +21 -5
- package/package.json +1 -1
|
@@ -3,10 +3,15 @@ import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
|
|
|
3
3
|
import { joinKeyFields } from "../shared/util/joinKeyFields.js";
|
|
4
4
|
/**
|
|
5
5
|
* Fixed-window rate limiter held in memory: the same windows, the same
|
|
6
|
-
* attempts-count semantics and the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* attempts-count semantics and the same refusals as LambderDdbRateLimiter,
|
|
7
|
+
* with a Map in place of the table. For tests and for the mock runtime; a
|
|
8
|
+
* single-process server could use it too, with the caveat that its counters
|
|
9
|
+
* are per process and vanish with it.
|
|
10
|
+
*
|
|
11
|
+
* Every counter is in hand, so an attempt a window refuses is counted in no
|
|
12
|
+
* window: what the DynamoDB limiter reaches once it remembers the refusing
|
|
13
|
+
* window, which the first refused attempt in each of its processes still
|
|
14
|
+
* counts in the others.
|
|
10
15
|
*
|
|
11
16
|
* `now` is injectable so a test can move time forward and watch a window
|
|
12
17
|
* reset without waiting for it.
|
|
@@ -31,24 +36,24 @@ export class LambderMemoryRateLimiter {
|
|
|
31
36
|
}
|
|
32
37
|
async isRateLimited(trackerKey, policy) {
|
|
33
38
|
const nowSeconds = Math.floor(this.now() / 1000);
|
|
34
|
-
|
|
35
|
-
const limit = policy[key];
|
|
36
|
-
if (!limit)
|
|
37
|
-
continue;
|
|
39
|
+
const windows = RATE_LIMIT_WINDOWS.filter(({ key }) => policy[key]).map(({ key, seconds }) => {
|
|
38
40
|
const windowStart = Math.floor(nowSeconds / seconds) * seconds;
|
|
39
41
|
// The table keeps the tracker key and the window in separate
|
|
40
42
|
// attributes; one string has to keep them as distinct, hence the
|
|
41
43
|
// escaping join.
|
|
42
44
|
const counterKey = joinKeyFields(trackerKey, key, String(windowStart));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
return { key, limit: policy[key], resetAt: windowStart + seconds, counterKey, counter: this.counters.get(counterKey) ?? { count: 0 } };
|
|
46
|
+
});
|
|
47
|
+
// The DynamoDB limiter's conditional ADD: allowed while the count
|
|
48
|
+
// before this attempt is under the limit, and the smallest window at
|
|
49
|
+
// its limit names the refusal.
|
|
50
|
+
const refusing = windows.find(({ counter, limit }) => counter.count >= limit);
|
|
51
|
+
if (refusing)
|
|
52
|
+
return { window: refusing.key, limit: refusing.limit, resetAt: refusing.resetAt };
|
|
53
|
+
for (const { counterKey, counter, resetAt } of windows) {
|
|
49
54
|
counter.count += 1;
|
|
50
55
|
// The counter dies with its window, which is also the table's TTL.
|
|
51
|
-
this.counters.set(counterKey, counter,
|
|
56
|
+
this.counters.set(counterKey, counter, resetAt);
|
|
52
57
|
}
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
@@ -9,6 +9,7 @@ import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.
|
|
|
9
9
|
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
10
10
|
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
11
11
|
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
12
|
+
import type { LambderCallSummary } from "../core/LambderCallSummary.js";
|
|
12
13
|
import { LambderTestVisitor, type LambderTestVisitorArgs, type LambderTestVisitorOptions } from "./LambderTestVisitor.js";
|
|
13
14
|
/**
|
|
14
15
|
* What a test app may be told. Nothing is required: `lambderTestApp(lambder)`
|
|
@@ -100,6 +101,8 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
100
101
|
private visitorCount;
|
|
101
102
|
private resetCount;
|
|
102
103
|
private readonly crashList;
|
|
104
|
+
/** Every call summary the app wrote since the last reset; see callSummaries. */
|
|
105
|
+
private readonly callSummaryList;
|
|
103
106
|
/**
|
|
104
107
|
* The call a crash happened under. The app's 500 says nothing about the
|
|
105
108
|
* crash, so the error travels beside the answer, and with concurrent
|
|
@@ -117,6 +120,12 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
117
120
|
* error an `event()` rejects with, which the test already holds.
|
|
118
121
|
*/
|
|
119
122
|
get crashes(): readonly Error[];
|
|
123
|
+
/**
|
|
124
|
+
* The summary of every API call the app answered since the last reset,
|
|
125
|
+
* in order (see LambderCallSummary): what its callSummary option would
|
|
126
|
+
* have been handed, collected here instead of written to stdout.
|
|
127
|
+
*/
|
|
128
|
+
get callSummaries(): readonly LambderCallSummary[];
|
|
120
129
|
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
|
|
121
130
|
get sessionManager(): LambderSessionManager<TSessionData>;
|
|
122
131
|
/**
|
|
@@ -159,10 +168,10 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
159
168
|
event(event: unknown, context?: Partial<Context>): Promise<unknown>;
|
|
160
169
|
/**
|
|
161
170
|
* Rewinds what accumulated: sessions, rate-limit counters and replay
|
|
162
|
-
* records in the stores this test app made, the crashes
|
|
163
|
-
* the cookies of every visitor it created
|
|
164
|
-
* time it is used). For a beforeEach. The
|
|
165
|
-
* is the app's to rewind.
|
|
171
|
+
* records in the stores this test app made, the crashes and call
|
|
172
|
+
* summaries it recorded, and the cookies of every visitor it created
|
|
173
|
+
* (each empties its jar the next time it is used). For a beforeEach. The
|
|
174
|
+
* app's own data (its database) is the app's to rewind.
|
|
166
175
|
*/
|
|
167
176
|
reset(): void;
|
|
168
177
|
}
|
|
@@ -49,6 +49,8 @@ export class LambderTestApp {
|
|
|
49
49
|
visitorCount = 0;
|
|
50
50
|
resetCount = 0;
|
|
51
51
|
crashList = [];
|
|
52
|
+
/** Every call summary the app wrote since the last reset; see callSummaries. */
|
|
53
|
+
callSummaryList = [];
|
|
52
54
|
/**
|
|
53
55
|
* The call a crash happened under. The app's 500 says nothing about the
|
|
54
56
|
* crash, so the error travels beside the answer, and with concurrent
|
|
@@ -66,7 +68,12 @@ export class LambderTestApp {
|
|
|
66
68
|
const sessionStore = options.session?.store ?? own(new LambderMemorySessionStore());
|
|
67
69
|
const rateLimiter = options.rateLimits?.limiter ?? own(new LambderMemoryRateLimiter());
|
|
68
70
|
const idempotencyStore = options.idempotency?.store ?? own(new LambderMemoryIdempotencyStore());
|
|
69
|
-
|
|
71
|
+
// The summaries are collected rather than written, so a suite's
|
|
72
|
+
// output stays its own and a test can read what a call recorded.
|
|
73
|
+
const swap = lambder[LAMBDER_BACKEND_SWAP]({
|
|
74
|
+
sessionStore, rateLimiter, idempotencyStore, fileSource: options.files,
|
|
75
|
+
callSummary: (summary) => { this.callSummaryList.push(summary); },
|
|
76
|
+
});
|
|
70
77
|
if (options.files && !swap.files) {
|
|
71
78
|
throw new Error("lambderTestApp: the files option was given, but the app was created without one, so nothing reads files to put a source under.");
|
|
72
79
|
}
|
|
@@ -116,6 +123,14 @@ export class LambderTestApp {
|
|
|
116
123
|
get crashes() {
|
|
117
124
|
return this.crashList;
|
|
118
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The summary of every API call the app answered since the last reset,
|
|
128
|
+
* in order (see LambderCallSummary): what its callSummary option would
|
|
129
|
+
* have been handed, collected here instead of written to stdout.
|
|
130
|
+
*/
|
|
131
|
+
get callSummaries() {
|
|
132
|
+
return this.callSummaryList;
|
|
133
|
+
}
|
|
119
134
|
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
|
|
120
135
|
get sessionManager() {
|
|
121
136
|
return this.lambder.getSessionManager();
|
|
@@ -178,15 +193,16 @@ export class LambderTestApp {
|
|
|
178
193
|
}
|
|
179
194
|
/**
|
|
180
195
|
* Rewinds what accumulated: sessions, rate-limit counters and replay
|
|
181
|
-
* records in the stores this test app made, the crashes
|
|
182
|
-
* the cookies of every visitor it created
|
|
183
|
-
* time it is used). For a beforeEach. The
|
|
184
|
-
* is the app's to rewind.
|
|
196
|
+
* records in the stores this test app made, the crashes and call
|
|
197
|
+
* summaries it recorded, and the cookies of every visitor it created
|
|
198
|
+
* (each empties its jar the next time it is used). For a beforeEach. The
|
|
199
|
+
* app's own data (its database) is the app's to rewind.
|
|
185
200
|
*/
|
|
186
201
|
reset() {
|
|
187
202
|
for (const store of this.ownStores)
|
|
188
203
|
store.reset();
|
|
189
204
|
this.crashList.length = 0;
|
|
205
|
+
this.callSummaryList.length = 0;
|
|
190
206
|
this.resetCount += 1;
|
|
191
207
|
}
|
|
192
208
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|