lambder 6.0.2 → 7.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 +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 +21 -19
- 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,221 @@
|
|
|
1
|
+
import { restoreCompressedPayload } from "./LambderApiRequest.js";
|
|
2
|
+
import { apiNotFoundAnswer, invalidPayloadAnswer, refusalAnswer, sessionExpiredAnswer, validationAnswer, versionExpiredAnswer, } from "./LambderApiEnvelope.js";
|
|
3
|
+
import { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./LambderApiValidationRefusal.js";
|
|
4
|
+
import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
5
|
+
import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderRequestPayload.js";
|
|
6
|
+
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
7
|
+
import { LambderApiPolicyEngine } from "./LambderApiPolicyEngine.js";
|
|
8
|
+
import LambderSessionController, { assertSessionCookiePrefixes, } from "../session/LambderSessionController.js";
|
|
9
|
+
import { DEFAULT_SESSION_CSRF_COOKIE_KEY, DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
|
|
10
|
+
/**
|
|
11
|
+
* The API pipeline: one API call from a parsed request to a plain answer,
|
|
12
|
+
* in the order the protocol defines. The Lambda server and the mock runtime
|
|
13
|
+
* are adapters over this class; neither reimplements a step of it.
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* version gate → restore payload → rate limits that need no session
|
|
17
|
+
* → session (session mode) → idempotency replay → the remaining rate limits
|
|
18
|
+
* → guards → input validation → exec, inside the idempotency claim
|
|
19
|
+
* → drain response headers → answer
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Steps whose subsystem is not configured are skipped. A LambderApiRefusal
|
|
23
|
+
* thrown by any step, guard or handler is rendered here, in one place: a
|
|
24
|
+
* validation error through onInvalidInput, any other refusal as the refusal
|
|
25
|
+
* envelope. Anything else propagates, because only the adapter knows what a
|
|
26
|
+
* crash means (a global error handler, a mock event).
|
|
27
|
+
*
|
|
28
|
+
* `run` never sees a name it has no definition for; resolving a name to a
|
|
29
|
+
* definition is the one thing the adapters legitimately do differently (an
|
|
30
|
+
* action list versus a registry), and answerUnknownApi is what they answer
|
|
31
|
+
* with.
|
|
32
|
+
*/
|
|
33
|
+
export class LambderApiPipeline {
|
|
34
|
+
apiVersion;
|
|
35
|
+
policies = new LambderApiPolicyEngine();
|
|
36
|
+
maxRequestPayloadBytes;
|
|
37
|
+
onInvalidInput;
|
|
38
|
+
sessions;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.apiVersion = options.apiVersion ?? null;
|
|
41
|
+
this.maxRequestPayloadBytes = assertPositiveInteger(options.maxRequestPayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "maxRequestPayloadBytes");
|
|
42
|
+
this.onInvalidInput = options.onInvalidInput ?? null;
|
|
43
|
+
this.sessions = options.sessions
|
|
44
|
+
? {
|
|
45
|
+
manager: options.sessions.manager,
|
|
46
|
+
tokenCookieKey: options.sessions.tokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY,
|
|
47
|
+
csrfCookieKey: options.sessions.csrfCookieKey ?? DEFAULT_SESSION_CSRF_COOKIE_KEY,
|
|
48
|
+
cookieOptions: options.sessions.cookieOptions ?? {},
|
|
49
|
+
}
|
|
50
|
+
: null;
|
|
51
|
+
if (this.sessions)
|
|
52
|
+
assertSessionCookiePrefixes(this.sessions);
|
|
53
|
+
if (options.rateLimits)
|
|
54
|
+
this.policies.configureRateLimits(options.rateLimits);
|
|
55
|
+
if (options.guards)
|
|
56
|
+
this.policies.configureGuards(options.guards);
|
|
57
|
+
if (options.idempotency)
|
|
58
|
+
this.policies.configureIdempotency(options.idempotency);
|
|
59
|
+
}
|
|
60
|
+
/** True when a session manager was configured. */
|
|
61
|
+
get hasSessions() { return this.sessions !== null; }
|
|
62
|
+
/** The session manager, for adapters that hand it out; throws when sessions are not configured. */
|
|
63
|
+
get sessionManager() {
|
|
64
|
+
if (!this.sessions)
|
|
65
|
+
throw new Error("Session is not enabled. Configure the session option at creation.");
|
|
66
|
+
return this.sessions.manager;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A session controller for one request: what handlers use to create,
|
|
70
|
+
* rotate, refresh and end sessions. The request info is the API request's
|
|
71
|
+
* (its cookies and posted CSRF token) or a route's (cookies and no CSRF).
|
|
72
|
+
*/
|
|
73
|
+
sessionController(ctx, request) {
|
|
74
|
+
if (!this.sessions)
|
|
75
|
+
throw new Error("Session is not enabled. Configure the session option at creation.");
|
|
76
|
+
return new LambderSessionController({
|
|
77
|
+
manager: this.sessions.manager,
|
|
78
|
+
tokenCookieKey: this.sessions.tokenCookieKey,
|
|
79
|
+
csrfCookieKey: this.sessions.csrfCookieKey,
|
|
80
|
+
cookieOptions: this.sessions.cookieOptions,
|
|
81
|
+
ctx,
|
|
82
|
+
request,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** The session request info of an API request: its cookies, and the CSRF token it posted. */
|
|
86
|
+
static sessionInfoOf(request) {
|
|
87
|
+
return { host: request.host, cookies: request.cookies, csrfToken: request.token };
|
|
88
|
+
}
|
|
89
|
+
/** Registration-time checks of one definition's declarative options; the same messages on the server and in the mock. */
|
|
90
|
+
assertRegistration(definition) {
|
|
91
|
+
this.policies.assertRegistration(definition);
|
|
92
|
+
}
|
|
93
|
+
/** True when the gate is on and the request names a different version. */
|
|
94
|
+
isVersionStale(request) {
|
|
95
|
+
return !!this.apiVersion && !!request.version && request.version !== this.apiVersion;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The answer for a request naming no registered API: the apiNotFound
|
|
99
|
+
* refusal, carrying whatever the call already wrote (a CORS header, a
|
|
100
|
+
* cookie eviction). No version gate here: both adapters run prepare() on
|
|
101
|
+
* the way in, before a name is resolved, so a stale client has already
|
|
102
|
+
* been answered by the time anything asks for an unknown name.
|
|
103
|
+
*/
|
|
104
|
+
answerUnknownApi(request, ctx) {
|
|
105
|
+
const answer = apiNotFoundAnswer(this.apiVersion, ctx?.logList);
|
|
106
|
+
ctx?.responseHeaders.applyInto(answer.headers);
|
|
107
|
+
return answer;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The steps that come before anything may read the request: the version
|
|
111
|
+
* gate, then the compressed-payload restore that every later reader (a
|
|
112
|
+
* rate-limit key slice, a guard, the input schema) depends on having
|
|
113
|
+
* happened.
|
|
114
|
+
*
|
|
115
|
+
* Public and named because the server runs them earlier than run() does,
|
|
116
|
+
* on the way in, so that its hooks see a plain payload and a stale client
|
|
117
|
+
* is answered before any of them, whether or not the name it asked for
|
|
118
|
+
* exists. run() calls it too, so an adapter that has no such step still
|
|
119
|
+
* gets the whole protocol. Calling it twice is safe by construction: the
|
|
120
|
+
* gate is a pure comparison and the restore has already removed the wire
|
|
121
|
+
* fields it reads.
|
|
122
|
+
*
|
|
123
|
+
* Returns the answer that ends the call, or null when the request is
|
|
124
|
+
* ready to dispatch.
|
|
125
|
+
*/
|
|
126
|
+
async prepare(request) {
|
|
127
|
+
if (this.isVersionStale(request))
|
|
128
|
+
return versionExpiredAnswer(this.apiVersion);
|
|
129
|
+
const restored = await restoreCompressedPayload(request, this.maxRequestPayloadBytes);
|
|
130
|
+
if (!restored.ok)
|
|
131
|
+
return invalidPayloadAnswer(this.apiVersion, restored.message);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* One call, one answer. Refusals are rendered; crashes propagate.
|
|
136
|
+
*
|
|
137
|
+
* An adapter that wants to report what the call did even when it crashed
|
|
138
|
+
* passes its own trace object: the pipeline writes into that one, so a
|
|
139
|
+
* handler that threw still leaves the guards it ran behind for the
|
|
140
|
+
* adapter's catch. Without it the trace was created here and lost with
|
|
141
|
+
* the throw, and the mock's call log showed no guards on exactly the
|
|
142
|
+
* calls a developer opens the log for.
|
|
143
|
+
*/
|
|
144
|
+
async run(request, ctx, definition, exec, trace = { guardsRun: [], replayed: false }) {
|
|
145
|
+
let answer;
|
|
146
|
+
try {
|
|
147
|
+
answer = await this.execute(request, ctx, definition, exec, trace);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (isLambderApiValidationRefusal(err)) {
|
|
151
|
+
answer = await this.refuseInput(err, ctx, request);
|
|
152
|
+
}
|
|
153
|
+
else if (isLambderApiRefusal(err)) {
|
|
154
|
+
answer = refusalAnswer(err, this.apiVersion, ctx.logList);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Every header written during the call belongs on the answer, whichever
|
|
161
|
+
// way it was produced: a cookie eviction from the session read, a
|
|
162
|
+
// handler's setHeader before it refused, the handler's own headers
|
|
163
|
+
// (already on it, so re-applying them here changes nothing).
|
|
164
|
+
ctx.responseHeaders.applyInto(answer.headers);
|
|
165
|
+
return { answer, ...trace };
|
|
166
|
+
}
|
|
167
|
+
async execute(request, ctx, definition, exec, trace) {
|
|
168
|
+
const unprepared = await this.prepare(request);
|
|
169
|
+
if (unprepared)
|
|
170
|
+
return unprepared;
|
|
171
|
+
// The limits whose key is known from the request alone, before the
|
|
172
|
+
// session store is asked anything: a request carrying bogus session
|
|
173
|
+
// cookies costs up to four store reads, and answering it
|
|
174
|
+
// sessionExpired without the limiter having run let one address spend
|
|
175
|
+
// the session store's read budget freely. A replay costs the same
|
|
176
|
+
// reads, so an ip-limited replay counts against that budget too: the
|
|
177
|
+
// limit protects the stores, not the handler.
|
|
178
|
+
await this.policies.runSessionlessRateLimits(request, ctx, definition);
|
|
179
|
+
if (definition.mode === "session") {
|
|
180
|
+
if (!this.sessions)
|
|
181
|
+
throw new Error(`Lambder: API "${definition.name}" is a session API, but no session store was configured at creation.`);
|
|
182
|
+
const session = await this.sessionController(ctx, LambderApiPipeline.sessionInfoOf(request)).fetchSessionIfExists();
|
|
183
|
+
if (!session)
|
|
184
|
+
return sessionExpiredAnswer(this.apiVersion, ctx.logList);
|
|
185
|
+
}
|
|
186
|
+
// Replay fast path: a completed idempotent request answers its stored
|
|
187
|
+
// answer without burning the remaining rate-limit quota or re-running
|
|
188
|
+
// guards. After the session read, because the replay scope is keyed
|
|
189
|
+
// per session.
|
|
190
|
+
const replay = await this.policies.findReplay(request, ctx, definition, trace);
|
|
191
|
+
if (replay)
|
|
192
|
+
return replay;
|
|
193
|
+
await this.policies.runPreflight(request, ctx, definition, trace);
|
|
194
|
+
if (definition.input) {
|
|
195
|
+
const parsed = definition.input.safeParse(request.payload);
|
|
196
|
+
if (!parsed.success)
|
|
197
|
+
throw new LambderApiValidationRefusal(parsed.error);
|
|
198
|
+
request.payload = parsed.data;
|
|
199
|
+
}
|
|
200
|
+
// The handler's own answer, and only that: what it wrote into
|
|
201
|
+
// responseHeaders during the call is on it before the idempotency
|
|
202
|
+
// engine judges and stores it, while a header written EARLIER in the
|
|
203
|
+
// call is not. That line matters, because the engine refuses to store
|
|
204
|
+
// an answer carrying a Set-Cookie: charge it with the stale-session
|
|
205
|
+
// cookie the session read evicted and an otherwise idempotent
|
|
206
|
+
// operation would silently stop being idempotent and re-execute on
|
|
207
|
+
// every retry. The call's earlier headers still reach the client;
|
|
208
|
+
// run() applies them to the answer on the way out.
|
|
209
|
+
const runHandler = async () => {
|
|
210
|
+
const handlerFirstHeader = ctx.responseHeaders.size;
|
|
211
|
+
const produced = await exec(ctx);
|
|
212
|
+
ctx.responseHeaders.applyInto(produced.headers, handlerFirstHeader);
|
|
213
|
+
return produced;
|
|
214
|
+
};
|
|
215
|
+
return await this.policies.withIdempotency(request, ctx, definition, trace, runHandler);
|
|
216
|
+
}
|
|
217
|
+
async refuseInput(err, ctx, request) {
|
|
218
|
+
const custom = this.onInvalidInput ? await this.onInvalidInput(err.zodError, ctx, request) : null;
|
|
219
|
+
return custom ?? validationAnswer(err.zodError, ctx.logList);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
2
|
+
import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCallContext.js";
|
|
3
|
+
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
4
|
+
import type { LambderApiDefinition } from "./LambderApiDefinition.js";
|
|
5
|
+
import { type LambderApiGuard } from "./LambderApiGuards.js";
|
|
6
|
+
import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig } from "./LambderApiRateLimits.js";
|
|
7
|
+
import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
|
|
8
|
+
/**
|
|
9
|
+
* Runtime side of the declarative API options: composes the three policy
|
|
10
|
+
* subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
|
|
11
|
+
* ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
|
|
12
|
+
* registrations against them at startup, and executes them around handlers
|
|
13
|
+
* at request time. Owned by LambderApiPipeline; apps interact through the
|
|
14
|
+
* create() options (rateLimits, guards, idempotency) and the per-API
|
|
15
|
+
* options.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LambderApiPolicyEngine {
|
|
18
|
+
private rateLimits;
|
|
19
|
+
private guards;
|
|
20
|
+
private idempotency;
|
|
21
|
+
/** True once any of the three subsystems was configured. */
|
|
22
|
+
get isConfigured(): boolean;
|
|
23
|
+
configureRateLimits(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
24
|
+
configureGuards(guards: Record<string, LambderApiGuard<any, any, any>>): void;
|
|
25
|
+
configureIdempotency(config: LambderApiIdempotencyConfig): void;
|
|
26
|
+
/** Startup validation of one API's declarative options. */
|
|
27
|
+
assertRegistration(definition: LambderApiDefinition): void;
|
|
28
|
+
/** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
|
|
29
|
+
runSessionlessRateLimits(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition): Promise<void>;
|
|
30
|
+
/** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
|
|
31
|
+
runPreflight(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<void>;
|
|
32
|
+
/** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
|
|
33
|
+
findReplay(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
|
|
34
|
+
/** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
|
|
35
|
+
withIdempotency(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LambderApiGuardsEngine } from "./LambderApiGuards.js";
|
|
2
|
+
import { LambderApiRateLimitsEngine } from "./LambderApiRateLimits.js";
|
|
3
|
+
import { LambderApiIdempotencyEngine } from "./LambderApiIdempotency.js";
|
|
4
|
+
/** An API that asks for idempotency: declared, and not the explicit `false` opt-out. */
|
|
5
|
+
const usesIdempotency = (definition) => definition.idempotency !== undefined && definition.idempotency !== false;
|
|
6
|
+
/**
|
|
7
|
+
* Runtime side of the declarative API options: composes the three policy
|
|
8
|
+
* subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
|
|
9
|
+
* ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
|
|
10
|
+
* registrations against them at startup, and executes them around handlers
|
|
11
|
+
* at request time. Owned by LambderApiPipeline; apps interact through the
|
|
12
|
+
* create() options (rateLimits, guards, idempotency) and the per-API
|
|
13
|
+
* options.
|
|
14
|
+
*/
|
|
15
|
+
export class LambderApiPolicyEngine {
|
|
16
|
+
rateLimits = new LambderApiRateLimitsEngine();
|
|
17
|
+
guards = new LambderApiGuardsEngine();
|
|
18
|
+
idempotency = new LambderApiIdempotencyEngine();
|
|
19
|
+
/** True once any of the three subsystems was configured. */
|
|
20
|
+
get isConfigured() {
|
|
21
|
+
return this.rateLimits.isConfigured || this.guards.isConfigured || this.idempotency.isConfigured;
|
|
22
|
+
}
|
|
23
|
+
configureRateLimits(config) {
|
|
24
|
+
this.rateLimits.configure(config);
|
|
25
|
+
}
|
|
26
|
+
configureGuards(guards) {
|
|
27
|
+
this.guards.configure(guards);
|
|
28
|
+
}
|
|
29
|
+
configureIdempotency(config) {
|
|
30
|
+
this.idempotency.configure(config);
|
|
31
|
+
}
|
|
32
|
+
/** Startup validation of one API's declarative options. */
|
|
33
|
+
assertRegistration(definition) {
|
|
34
|
+
const { name, mode } = definition;
|
|
35
|
+
// Each subsystem reports its own absence. One combined message would
|
|
36
|
+
// tell an API that declares guards on an instance with no guards map
|
|
37
|
+
// that none of the three was configured, which reads as a question
|
|
38
|
+
// about all three when only one of them is missing.
|
|
39
|
+
if (definition.rateLimit !== undefined && !this.rateLimits.isConfigured) {
|
|
40
|
+
throw new Error(`Lambder: API "${name}" declares rateLimit but no rateLimits option was configured at creation.`);
|
|
41
|
+
}
|
|
42
|
+
if (definition.guards !== undefined && !this.guards.isConfigured) {
|
|
43
|
+
throw new Error(`Lambder: API "${name}" declares guards but no guards option was configured at creation.`);
|
|
44
|
+
}
|
|
45
|
+
this.rateLimits.assertRegistration(name, mode, definition.rateLimit);
|
|
46
|
+
this.guards.assertRegistration(name, mode, definition.guards);
|
|
47
|
+
// `idempotency: false` is an explicit opt-out, not a use: it asks for
|
|
48
|
+
// nothing and so needs no store behind it.
|
|
49
|
+
if (usesIdempotency(definition)) {
|
|
50
|
+
if (!this.idempotency.isConfigured) {
|
|
51
|
+
throw new Error(`Lambder: API "${name}" declares idempotency but no idempotency store was configured at creation.`);
|
|
52
|
+
}
|
|
53
|
+
this.idempotency.assertRegistration(name, definition.idempotency);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
|
|
57
|
+
async runSessionlessRateLimits(request, ctx, definition) {
|
|
58
|
+
await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "beforeSession");
|
|
59
|
+
}
|
|
60
|
+
/** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
|
|
61
|
+
async runPreflight(request, ctx, definition, trace) {
|
|
62
|
+
await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "afterSession");
|
|
63
|
+
await this.guards.run(request, ctx, definition.guards, trace);
|
|
64
|
+
}
|
|
65
|
+
/** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
|
|
66
|
+
async findReplay(request, ctx, definition, trace) {
|
|
67
|
+
if (!definition.idempotency)
|
|
68
|
+
return null;
|
|
69
|
+
return await this.idempotency.findReplay(definition.name, request, ctx, trace);
|
|
70
|
+
}
|
|
71
|
+
/** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
|
|
72
|
+
async withIdempotency(request, ctx, definition, trace, exec) {
|
|
73
|
+
if (!definition.idempotency)
|
|
74
|
+
return await exec();
|
|
75
|
+
return await this.idempotency.withIdempotency(definition.name, request, ctx, definition.idempotency, trace, exec);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
|
|
2
|
+
import type { LambderRateLimitOptionValue, LambderRateLimitOverride } from "../shared/wire/LambderApiOptionValues.js";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
5
|
+
import type { LambderApiCallContext } from "./LambderApiCallContext.js";
|
|
6
|
+
import { type LambderRateLimiter, type LambderRateLimitPolicy } from "../shared/contracts/LambderRateLimiter.js";
|
|
7
|
+
import { LambderApiRefusal, type LambderAppRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
|
|
8
|
+
import type { LambderNonEmptyOptionMap } from "../shared/util/LambderTypeUtilities.js";
|
|
9
|
+
/** Refusal a rate-limited request answers unless the policy or the API's override names its own. */
|
|
10
|
+
export declare const DEFAULT_RATE_LIMIT_REFUSAL: {
|
|
11
|
+
type: "warning";
|
|
12
|
+
code: "lambder/rate-limited";
|
|
13
|
+
content: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The refusal a rate-limited call answers with: a 429 envelope carrying the
|
|
17
|
+
* framework code (a policy's own message inherits it unless it sets a more
|
|
18
|
+
* specific one) and a Retry-After header. The engine throws it; the mock
|
|
19
|
+
* runtime's failure injection throws the same one, so an injected rate
|
|
20
|
+
* limit is indistinguishable from a real one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const rateLimitRefusal: (detail: string, retryAfterSeconds: number, message?: LambderAppRefusalMessage) => LambderApiRefusal;
|
|
23
|
+
/**
|
|
24
|
+
* A custom rate-limit key. `apiInput` names the fields of the API's OWN
|
|
25
|
+
* payload the key derives from: the slice is validated against the raw
|
|
26
|
+
* payload before `handler` runs (failures answer like regular input
|
|
27
|
+
* validation, through setApiInputValidationErrorHandler when set) and the
|
|
28
|
+
* handler receives it typed. Referencing the policy from an API whose input
|
|
29
|
+
* schema does not carry those fields is a compile error, so the API's schema
|
|
30
|
+
* stays the single owner of the field. Build with lambderRateLimitKey() so
|
|
31
|
+
* the handler's payload type follows `apiInput`. The context is the
|
|
32
|
+
* adapter's (the render context on the server); the engine reads nothing
|
|
33
|
+
* from it itself.
|
|
34
|
+
*
|
|
35
|
+
* ONE member, with `apiInput` optional, rather than a union of the two
|
|
36
|
+
* shapes: a union with a function member in each arm defeats contextual
|
|
37
|
+
* typing, so annotating a policies map with LambderRateLimitPer or
|
|
38
|
+
* LambderApiRateLimitPolicyConfig left `ctx` implicitly any and the
|
|
39
|
+
* annotation did not compile at all. The builder's overloads are where the
|
|
40
|
+
* apiInput/payload correlation is kept.
|
|
41
|
+
*/
|
|
42
|
+
export type LambderRateLimitKeyFn<TInput extends z.ZodType = z.ZodType, TCtx = any> = {
|
|
43
|
+
apiInput?: TInput;
|
|
44
|
+
handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Builder that ties the handler's payload type to the `apiInput` schema
|
|
48
|
+
* inside one literal. Returns the exact union member so type extraction can
|
|
49
|
+
* see the schema.
|
|
50
|
+
*/
|
|
51
|
+
export type LambderRateLimitKeyBuilder<TCtx> = {
|
|
52
|
+
<TInput extends z.ZodType>(key: {
|
|
53
|
+
apiInput: TInput;
|
|
54
|
+
handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
|
|
55
|
+
}): {
|
|
56
|
+
apiInput: TInput;
|
|
57
|
+
handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
|
|
58
|
+
};
|
|
59
|
+
(key: {
|
|
60
|
+
handler: (ctx: TCtx, payload: undefined) => string | Promise<string>;
|
|
61
|
+
}): {
|
|
62
|
+
apiInput?: undefined;
|
|
63
|
+
handler: (ctx: TCtx, payload: undefined) => string | Promise<string>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* A key builder bound to a context type, the counterpart of
|
|
68
|
+
* lambderGuardBuilder. The server's lambderRateLimitKey() is this bound to
|
|
69
|
+
* the render context; the mock runtime binds it to its own call context and
|
|
70
|
+
* exposes it as `rateLimitKey`.
|
|
71
|
+
*
|
|
72
|
+
* Bound rather than left open because the engine hands the handler whatever
|
|
73
|
+
* context the adapter runs on, and the two adapters run on different ones. A
|
|
74
|
+
* single builder pinned to the server's context type compiled against the
|
|
75
|
+
* mock and then handed the handler a context with no `ip`, `method` or
|
|
76
|
+
* `path`, so every caller collapsed onto one counter and the limit a test was
|
|
77
|
+
* written to prove silently proved nothing.
|
|
78
|
+
*/
|
|
79
|
+
export declare const lambderRateLimitKeyBuilder: <TCtx>() => LambderRateLimitKeyBuilder<TCtx>;
|
|
80
|
+
/** What one rate-limit counter tracks: the client IP, the session identity, or a custom payload-derived key. */
|
|
81
|
+
export type LambderRateLimitPer<TCtx = any> = "ip" | "session" | LambderRateLimitKeyFn<any, TCtx>;
|
|
82
|
+
/**
|
|
83
|
+
* What one budget spans:
|
|
84
|
+
*
|
|
85
|
+
* - "perApi" (default): every API referencing the policy gets its own
|
|
86
|
+
* counter, so the windows are a per-API ceiling (three APIs referencing a
|
|
87
|
+
* 60/min policy allow one subject 180/min in total). An API may tune the
|
|
88
|
+
* windows in its declaration: `rateLimit: { name: { perMin: 20 } }`.
|
|
89
|
+
* - "perPolicy": every API referencing the policy shares ONE counter, so the
|
|
90
|
+
* windows are one combined budget (e.g. one per-email allowance across
|
|
91
|
+
* send, register, and reset). The policy IS the group: to give user APIs
|
|
92
|
+
* and report APIs separate shared budgets, declare two policies.
|
|
93
|
+
*/
|
|
94
|
+
export type LambderRateLimitBudget = "perApi" | "perPolicy";
|
|
95
|
+
/**
|
|
96
|
+
* A named rate-limit policy: fixed windows, the key one counter tracks, and
|
|
97
|
+
* what one budget spans.
|
|
98
|
+
*
|
|
99
|
+
* Generic over the context a custom key handler receives, so the adapter's
|
|
100
|
+
* policies map pins it: the server's is the render context, the mock's is the
|
|
101
|
+
* mock call context. Left open, a handler written for one adapter compiled
|
|
102
|
+
* against the other and then read fields that were not there.
|
|
103
|
+
*/
|
|
104
|
+
export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy & {
|
|
105
|
+
per: LambderRateLimitPer<TCtx>;
|
|
106
|
+
/** Whether the windows are a per-API ceiling (default) or one budget shared by every referencing API. See LambderRateLimitBudget. */
|
|
107
|
+
budget?: LambderRateLimitBudget;
|
|
108
|
+
/** Envelope errorMessage for refused requests; inherits code "lambder/rate-limited" unless it sets its own. Default: a warning saying too many requests. */
|
|
109
|
+
errorMessage?: LambderAppRefusalMessage;
|
|
110
|
+
};
|
|
111
|
+
export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
|
|
112
|
+
/** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
|
|
113
|
+
limiter: LambderRateLimiter;
|
|
114
|
+
/** Named policies referenced (typed) from addApi/addSessionApi. */
|
|
115
|
+
policies: TPolicies;
|
|
116
|
+
/**
|
|
117
|
+
* Let the request through when the limiter itself fails (the table is
|
|
118
|
+
* down, an IAM action is missing), instead of failing the request.
|
|
119
|
+
* Default: true, and the failure is logged either way.
|
|
120
|
+
*
|
|
121
|
+
* It lives here rather than on a limiter implementation because it is a
|
|
122
|
+
* decision about the REQUEST, not about a store: a custom limiter had no
|
|
123
|
+
* fail-open at all, and two limiters could answer the same outage
|
|
124
|
+
* differently. Set it to false on an app where an unmetered request is
|
|
125
|
+
* worse than a refused one.
|
|
126
|
+
*/
|
|
127
|
+
failOpen?: boolean;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Policy names an API may reference: session-keyed policies only on session
|
|
131
|
+
* APIs, and apiInput-keyed policies only when the API's payload carries the
|
|
132
|
+
* key's fields.
|
|
133
|
+
*/
|
|
134
|
+
export type LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession extends boolean> = {
|
|
135
|
+
[K in keyof TPolicies]: TPolicies[K] extends {
|
|
136
|
+
per: "session";
|
|
137
|
+
} ? (TIncludeSession extends true ? K : never) : TPolicies[K] extends {
|
|
138
|
+
per: {
|
|
139
|
+
apiInput: infer S extends z.ZodType;
|
|
140
|
+
};
|
|
141
|
+
} ? (TPayload extends z.output<S> ? K : never) : K;
|
|
142
|
+
}[keyof TPolicies] & string;
|
|
143
|
+
type LambderRateLimitOverrideFor<TPolicy> = TPolicy extends {
|
|
144
|
+
budget: "perPolicy";
|
|
145
|
+
} ? Pick<LambderRateLimitOverride, "errorMessage"> : LambderRateLimitOverride;
|
|
146
|
+
/** The map form's full shape: every referable policy name, each carrying its own override. */
|
|
147
|
+
type LambderRateLimitMap<TPolicies, TPayload, TIncludeSession extends boolean> = {
|
|
148
|
+
readonly [K in LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The per-API `rateLimit` option: one policy name, a non-empty ordered list
|
|
152
|
+
* of names, or a non-empty object map that can carry each policy's override
|
|
153
|
+
* (`true` applies the policy as declared). Map entries are checked in
|
|
154
|
+
* insertion order.
|
|
155
|
+
*
|
|
156
|
+
* Every form is non-empty by construction, the same machinery the guards
|
|
157
|
+
* option uses (LambderNonEmptyOptionMap): `rateLimit: {}`, `rateLimit: []`
|
|
158
|
+
* and `rateLimit: { policy: undefined }` announce a limit and enforce none.
|
|
159
|
+
*/
|
|
160
|
+
export type LambderRateLimitOption<TPolicies, TPayload, TIncludeSession extends boolean> = LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> | readonly [
|
|
161
|
+
LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>,
|
|
162
|
+
...LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>[]
|
|
163
|
+
] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies, TPayload, TIncludeSession>>;
|
|
164
|
+
/**
|
|
165
|
+
* When in a call a policy can be checked. A `per: "ip"` counter is known from
|
|
166
|
+
* the request alone, so it runs before the session read and bounds how often
|
|
167
|
+
* one address may make the session store look a token up. Everything else
|
|
168
|
+
* runs after: `per: "session"` needs the session, and a custom key handler is
|
|
169
|
+
* app code that may read ctx.session too.
|
|
170
|
+
*/
|
|
171
|
+
type LambderRateLimitPhase = "beforeSession" | "afterSession";
|
|
172
|
+
/**
|
|
173
|
+
* Runtime side of the rate-limit subsystem: holds the limiter and its named
|
|
174
|
+
* policies, asserts API registrations against them at startup, and checks an
|
|
175
|
+
* API's declared policies during preflight. Composed into
|
|
176
|
+
* LambderApiPolicyEngine. Reads the request's ip and the context's session
|
|
177
|
+
* and nothing else, so it runs unchanged under the server and the mock
|
|
178
|
+
* runtime.
|
|
179
|
+
*/
|
|
180
|
+
export declare class LambderApiRateLimitsEngine {
|
|
181
|
+
private limiter;
|
|
182
|
+
private failOpen;
|
|
183
|
+
private policies;
|
|
184
|
+
/** True once rateLimits were configured. */
|
|
185
|
+
get isConfigured(): boolean;
|
|
186
|
+
configure(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
187
|
+
/** Startup validation of one API registration's rateLimit option. */
|
|
188
|
+
assertRegistration(apiName: string, mode: LambderApiMode, rateLimitOption?: LambderRateLimitOptionValue): void;
|
|
189
|
+
/**
|
|
190
|
+
* Check the API's policies in declared order; the first exceeded one
|
|
191
|
+
* refuses with a 429 envelope and a Retry-After header. Attempts count,
|
|
192
|
+
* not successes: every counter checked before the refusing one (and every
|
|
193
|
+
* counter, when a later guard or validation refuses) keeps its increment,
|
|
194
|
+
* so list first the policy you want charged on refusals.
|
|
195
|
+
*
|
|
196
|
+
* Run twice per call, once per phase: the policies whose key needs no
|
|
197
|
+
* session are checked BEFORE the session is read, so a flood of requests
|
|
198
|
+
* carrying bogus session cookies is refused without touching the session
|
|
199
|
+
* store; the rest are checked after it, since `per: "session"` and a
|
|
200
|
+
* custom key handler may both read ctx.session. Declared order is kept
|
|
201
|
+
* inside each phase.
|
|
202
|
+
*/
|
|
203
|
+
run(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext, rateLimitOption: LambderRateLimitOptionValue | undefined, phase: LambderRateLimitPhase): Promise<void>;
|
|
204
|
+
private resolveKey;
|
|
205
|
+
}
|
|
206
|
+
export {};
|