lambder 15.0.2 → 16.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 +118 -0
- package/README.md +4 -4
- package/dist/api/LambderApiDeclarations.d.ts +21 -5
- package/dist/api/LambderApiDeclarations.js +21 -3
- package/dist/api/LambderApiGuards.d.ts +23 -3
- package/dist/api/LambderApiGuards.js +50 -25
- package/dist/api/LambderApiModules.d.ts +50 -0
- package/dist/api/LambderApiModules.js +36 -0
- package/dist/api/LambderApiPipeline.d.ts +27 -4
- package/dist/api/LambderApiPipeline.js +71 -5
- package/dist/api/LambderApiRateLimits.d.ts +35 -4
- package/dist/api/LambderApiRateLimits.js +74 -35
- package/dist/core/Lambder.d.ts +68 -93
- package/dist/core/Lambder.js +147 -71
- package/dist/core/LambderApiBuilders.d.ts +105 -0
- package/dist/core/LambderApiBuilders.js +12 -0
- package/dist/core/LambderApiModule.d.ts +106 -0
- package/dist/core/LambderApiModule.js +45 -0
- package/dist/core/LambderCallSummary.d.ts +11 -9
- package/dist/core/LambderCreateOptions.d.ts +29 -24
- package/dist/core/LambderInvocationScope.d.ts +9 -3
- package/dist/core/LambderRouting.d.ts +9 -0
- package/dist/core/LambderRouting.js +20 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2 -0
- package/dist/mock/LambderMockApiModule.d.ts +70 -0
- package/dist/mock/LambderMockApiModule.js +34 -0
- package/dist/mock/LambderMockApp.d.ts +53 -8
- package/dist/mock/LambderMockApp.js +104 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +18 -18
- package/dist/mock/LambderMockTypes.d.ts +46 -26
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +2 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/testing/LambderTestApp.d.ts +4 -3
- package/dist/testing/LambderTestApp.js +4 -3
- package/package.json +1 -1
|
@@ -60,6 +60,8 @@ export class LambderApiPipeline {
|
|
|
60
60
|
onInvalidInput;
|
|
61
61
|
sessions;
|
|
62
62
|
apiSignatures;
|
|
63
|
+
/** The modules whose guards and rate-limit policies this pipeline took (addModules), each once. */
|
|
64
|
+
modules = new Set();
|
|
63
65
|
constructor(options = {}) {
|
|
64
66
|
this.apiVersion = options.apiVersion ?? null;
|
|
65
67
|
// Dotted, always, whether or not a floor is set today: the floor reads
|
|
@@ -160,19 +162,83 @@ export class LambderApiPipeline {
|
|
|
160
162
|
async chargeRateLimit(name, subject) {
|
|
161
163
|
return await this.rateLimits.chargePolicy(name, subject);
|
|
162
164
|
}
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
+
/** Whether this pipeline took the module's guards and rate-limit policies already. */
|
|
166
|
+
hasModule(module) {
|
|
167
|
+
return this.modules.has(module);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The modules among `uses` this pipeline has not taken yet, checked and
|
|
171
|
+
* merged, and nothing taken: a name one of them declares that this
|
|
172
|
+
* pipeline declares already (at creation, or by a module taken before)
|
|
173
|
+
* or that another of them declares is refused, as create() refuses a name
|
|
174
|
+
* two maps declare, and so are rate-limit policies on a pipeline with no
|
|
175
|
+
* limiter for them. A module met twice is one module. addModules() takes
|
|
176
|
+
* what this returns once every endpoint of the registration passed.
|
|
177
|
+
*/
|
|
178
|
+
modulesToAdd(uses) {
|
|
179
|
+
const modules = [];
|
|
180
|
+
const guards = Object.create(null);
|
|
181
|
+
const rateLimitPolicies = Object.create(null);
|
|
182
|
+
const guardModules = new Map();
|
|
183
|
+
const policyModules = new Map();
|
|
184
|
+
const claim = (what, name, usedBy, declaredHere, claimed) => {
|
|
185
|
+
if (declaredHere) {
|
|
186
|
+
throw new Error(`Lambder: the ${what} "${name}" of the module ${usedBy} is declared already, at creation or by a module registered before. ` +
|
|
187
|
+
`A module declares names of its own: rename one of the two.`);
|
|
188
|
+
}
|
|
189
|
+
const other = claimed.get(name);
|
|
190
|
+
if (other !== undefined) {
|
|
191
|
+
throw new Error(`Lambder: the ${what} "${name}" is declared by two modules, the one ${other} and the one ${usedBy}. Declare each name once.`);
|
|
192
|
+
}
|
|
193
|
+
claimed.set(name, usedBy);
|
|
194
|
+
};
|
|
195
|
+
for (const { module, usedBy } of uses) {
|
|
196
|
+
if (this.modules.has(module) || modules.includes(module))
|
|
197
|
+
continue;
|
|
198
|
+
for (const [name, guard] of Object.entries(module.guards)) {
|
|
199
|
+
claim("guard", name, usedBy, this.guards.has(name), guardModules);
|
|
200
|
+
guards[name] = guard;
|
|
201
|
+
}
|
|
202
|
+
const policies = Object.entries(module.rateLimitPolicies);
|
|
203
|
+
if (policies.length > 0 && !this.rateLimits.isConfigured) {
|
|
204
|
+
throw new Error(`Lambder: the module ${usedBy} declares rate-limit policies, and no rateLimits option was given at creation, ` +
|
|
205
|
+
`which gives the limiter they count on. Pass rateLimits at creation; on a server its own policies may be left out (rateLimits: { limiter }).`);
|
|
206
|
+
}
|
|
207
|
+
for (const [name, policy] of policies) {
|
|
208
|
+
claim("rate-limit policy", name, usedBy, this.rateLimits.has(name), policyModules);
|
|
209
|
+
rateLimitPolicies[name] = policy;
|
|
210
|
+
}
|
|
211
|
+
modules.push(module);
|
|
212
|
+
}
|
|
213
|
+
return { modules, guards, rateLimitPolicies };
|
|
214
|
+
}
|
|
215
|
+
/** Takes the guards and rate-limit policies of the modules modulesToAdd() checked: from here on its endpoints run them, and code may charge the policies. */
|
|
216
|
+
addModules(added) {
|
|
217
|
+
this.guards.add(added.guards);
|
|
218
|
+
if (Object.keys(added.rateLimitPolicies).length > 0)
|
|
219
|
+
this.rateLimits.add(added.rateLimitPolicies);
|
|
220
|
+
for (const module of added.modules)
|
|
221
|
+
this.modules.add(module);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Registration-time checks of one definition's declarative options; the
|
|
225
|
+
* same messages on the server and in the mock. `adding` holds the guards
|
|
226
|
+
* and policies a registration is about to add beside this pipeline's own:
|
|
227
|
+
* those of the modules it brings (modulesToAdd), or of the module an
|
|
228
|
+
* entry is built with.
|
|
229
|
+
*/
|
|
230
|
+
assertRegistration(definition, adding) {
|
|
165
231
|
const { name, mode } = definition;
|
|
166
232
|
// Each subsystem reports its own absence: one combined message would
|
|
167
233
|
// name all three when only one of them is missing.
|
|
168
234
|
if (definition.rateLimit !== undefined && !this.rateLimits.isConfigured) {
|
|
169
235
|
throw new Error(`Lambder: API "${name}" declares rateLimit but no rateLimits option was configured at creation.`);
|
|
170
236
|
}
|
|
171
|
-
if (definition.guards !== undefined && !this.guards.isConfigured) {
|
|
237
|
+
if (definition.guards !== undefined && !this.guards.isConfigured && Object.keys(adding?.guards ?? {}).length === 0) {
|
|
172
238
|
throw new Error(`Lambder: API "${name}" declares guards but no guards option was configured at creation.`);
|
|
173
239
|
}
|
|
174
|
-
this.rateLimits.assertRegistration(name, mode, definition.rateLimit);
|
|
175
|
-
this.guards.assertRegistration(name, mode, definition.guards);
|
|
240
|
+
this.rateLimits.assertRegistration(name, mode, definition.rateLimit, adding?.rateLimitPolicies);
|
|
241
|
+
this.guards.assertRegistration(name, mode, definition.guards, adding?.guards);
|
|
176
242
|
// `idempotency: false` is an explicit opt-out, not a use: it asks for
|
|
177
243
|
// nothing and so needs no store behind it.
|
|
178
244
|
if (usesIdempotency(definition)) {
|
|
@@ -118,8 +118,12 @@ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy
|
|
|
118
118
|
export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
|
|
119
119
|
/** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
|
|
120
120
|
limiter: LambderRateLimiter;
|
|
121
|
-
/**
|
|
122
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Named policies referenced (typed) from defineApi. Left out where every
|
|
123
|
+
* policy is declared by the app's modules (defineApiModule), which count
|
|
124
|
+
* on this limiter; an empty map is refused.
|
|
125
|
+
*/
|
|
126
|
+
policies?: TPolicies;
|
|
123
127
|
/**
|
|
124
128
|
* Let the request through when the limiter itself fails (the table is
|
|
125
129
|
* down, an IAM action is missing), instead of failing the request.
|
|
@@ -270,6 +274,14 @@ export type LambderRateLimitOption<TPolicies> = LambderAllowedPolicyNames<TPolic
|
|
|
270
274
|
* session-keyed limits, before the guards, for "beforeGuards".
|
|
271
275
|
*/
|
|
272
276
|
type LambderRateLimitPhase = "beforeSession" | LambderRateLimitChargeAt;
|
|
277
|
+
/**
|
|
278
|
+
* Refuses a policy that cannot be counted: a per that is no key, no window,
|
|
279
|
+
* a window no limiter can act on, a chargeAt on a policy that takes none, a
|
|
280
|
+
* message carrying a code or data, an unknown budget. What the policies
|
|
281
|
+
* given at creation and a module's policies (defineApiModule) are each held
|
|
282
|
+
* to, one rule for both.
|
|
283
|
+
*/
|
|
284
|
+
export declare const assertRateLimitPolicyDeclaration: (name: string, policy: LambderApiRateLimitPolicyConfig) => void;
|
|
273
285
|
/**
|
|
274
286
|
* Runtime side of the rate-limit subsystem: holds the limiter and its named
|
|
275
287
|
* policies, asserts API registrations against them at startup, and checks an
|
|
@@ -299,15 +311,34 @@ export declare class LambderApiRateLimitsEngine {
|
|
|
299
311
|
constructor(keyFieldDigest: LambderKeyFieldDigest);
|
|
300
312
|
/** True once rateLimits were configured. */
|
|
301
313
|
get isConfigured(): boolean;
|
|
314
|
+
/** Whether a policy of this name is declared, by the creation's policies or a module added since. */
|
|
315
|
+
has(name: string): boolean;
|
|
316
|
+
/**
|
|
317
|
+
* Takes the limiter and the policies given at creation. The policies may
|
|
318
|
+
* be left out when every policy is a module's (defineApiModule), which
|
|
319
|
+
* counts on this limiter; an empty map is refused, as an empty guards
|
|
320
|
+
* map is.
|
|
321
|
+
*/
|
|
302
322
|
configure(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
323
|
+
/**
|
|
324
|
+
* Adds a module's policies beside the ones declared so far, counted on
|
|
325
|
+
* the limiter given at creation. Its names were checked against them
|
|
326
|
+
* before (LambderApiPipeline.modulesToAdd), so a name met again here is a
|
|
327
|
+
* caller that skipped that check.
|
|
328
|
+
*/
|
|
329
|
+
add(policies: Readonly<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
303
330
|
/**
|
|
304
331
|
* Puts the engine over another limiter, for `lambder/testing`; the named
|
|
305
332
|
* policies and failOpen stay as configured. False when rateLimits were
|
|
306
333
|
* never configured: there is nothing for a limiter to sit under.
|
|
307
334
|
*/
|
|
308
335
|
[LAMBDER_BACKEND_SWAP](limiter: LambderRateLimiter): boolean;
|
|
309
|
-
/**
|
|
310
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Startup validation of one API registration's rateLimit option.
|
|
338
|
+
* `adding` holds the policies of the modules the same registration adds,
|
|
339
|
+
* which the engine takes only once every endpoint of it passed.
|
|
340
|
+
*/
|
|
341
|
+
assertRegistration(apiName: string, mode: LambderApiMode, rateLimitOption?: LambderRateLimitOptionValue, adding?: Readonly<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
311
342
|
/**
|
|
312
343
|
* Check the API's policies in declared order; the first exceeded one
|
|
313
344
|
* refuses with a 429 envelope and a Retry-After header. Attempts count,
|
|
@@ -123,6 +123,37 @@ const windowsOf = (policy, override) => {
|
|
|
123
123
|
const trackerKeyOf = (name, policy, apiName, key) => policy.budget === "perPolicy" || apiName === null
|
|
124
124
|
? joinKeyFields("policy", name, key)
|
|
125
125
|
: joinKeyFields("api", apiName, name, key);
|
|
126
|
+
/**
|
|
127
|
+
* Refuses a policy that cannot be counted: a per that is no key, no window,
|
|
128
|
+
* a window no limiter can act on, a chargeAt on a policy that takes none, a
|
|
129
|
+
* message carrying a code or data, an unknown budget. What the policies
|
|
130
|
+
* given at creation and a module's policies (defineApiModule) are each held
|
|
131
|
+
* to, one rule for both.
|
|
132
|
+
*/
|
|
133
|
+
export const assertRateLimitPolicyDeclaration = (name, policy) => {
|
|
134
|
+
const per = policy?.per;
|
|
135
|
+
if (per !== undefined && per !== "ip" && per !== "session" && typeof per?.handler !== "function") {
|
|
136
|
+
throw new Error(`Lambder: rate-limit policy "${name}" has a per that is not "ip", "session", or a { apiInput?, handler } key. Leave per out for a policy the handler charges with its own key.`);
|
|
137
|
+
}
|
|
138
|
+
if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
|
|
139
|
+
throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
|
|
140
|
+
}
|
|
141
|
+
assertWindowLimits(`rate-limit policy "${name}"`, policy);
|
|
142
|
+
const chargeAt = policy.chargeAt;
|
|
143
|
+
if (chargeAt !== undefined) {
|
|
144
|
+
if (chargeAt !== "beforeGuards" && chargeAt !== "afterGuards") {
|
|
145
|
+
throw new Error(`Lambder: rate-limit policy "${name}" has chargeAt "${String(chargeAt)}"; use "afterGuards" (default) or "beforeGuards".`);
|
|
146
|
+
}
|
|
147
|
+
if (per === undefined || per === "ip" || per === "session") {
|
|
148
|
+
throw new Error(`Lambder: rate-limit policy "${name}" sets chargeAt, which only a policy keyed by a { apiInput?, handler } key takes: per "ip" and per "session" each run at one fixed place, and a policy without per is charged by the code that names it.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
assertRateLimitMessage(`rate-limit policy "${name}"`, policy.refusal);
|
|
152
|
+
const budget = policy.budget;
|
|
153
|
+
if (budget !== undefined && budget !== "perApi" && budget !== "perPolicy") {
|
|
154
|
+
throw new Error(`Lambder: rate-limit policy "${name}" has budget "${String(budget)}"; use "perApi" (default: each referencing API counts separately) or "perPolicy" (one counter shared by every referencing API).`);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
126
157
|
/**
|
|
127
158
|
* Runtime side of the rate-limit subsystem: holds the limiter and its named
|
|
128
159
|
* policies, asserts API registrations against them at startup, and checks an
|
|
@@ -157,40 +188,27 @@ export class LambderApiRateLimitsEngine {
|
|
|
157
188
|
}
|
|
158
189
|
/** True once rateLimits were configured. */
|
|
159
190
|
get isConfigured() { return this.limiter !== null; }
|
|
191
|
+
/** Whether a policy of this name is declared, by the creation's policies or a module added since. */
|
|
192
|
+
has(name) { return this.policies.has(name); }
|
|
193
|
+
/**
|
|
194
|
+
* Takes the limiter and the policies given at creation. The policies may
|
|
195
|
+
* be left out when every policy is a module's (defineApiModule), which
|
|
196
|
+
* counts on this limiter; an empty map is refused, as an empty guards
|
|
197
|
+
* map is.
|
|
198
|
+
*/
|
|
160
199
|
configure(config) {
|
|
161
200
|
if (this.limiter)
|
|
162
201
|
throw new Error("Lambder: rateLimits were already configured.");
|
|
163
|
-
// Declaring the
|
|
164
|
-
// and an API's own `rateLimit: {}`. An empty map would
|
|
165
|
-
// limiter with nothing to enforce, and every API naming a
|
|
166
|
-
// be reported as if the option were missing: the wrong
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
for (const [name, policy] of Object.entries(config.policies)) {
|
|
171
|
-
const per = policy.per;
|
|
172
|
-
if (per !== undefined && per !== "ip" && per !== "session" && typeof per?.handler !== "function") {
|
|
173
|
-
throw new Error(`Lambder: rate-limit policy "${name}" has a per that is not "ip", "session", or a { apiInput?, handler } key. Leave per out for a policy the handler charges with its own key.`);
|
|
174
|
-
}
|
|
175
|
-
if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
|
|
176
|
-
throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
|
|
177
|
-
}
|
|
178
|
-
assertWindowLimits(`rate-limit policy "${name}"`, policy);
|
|
179
|
-
const chargeAt = policy.chargeAt;
|
|
180
|
-
if (chargeAt !== undefined) {
|
|
181
|
-
if (chargeAt !== "beforeGuards" && chargeAt !== "afterGuards") {
|
|
182
|
-
throw new Error(`Lambder: rate-limit policy "${name}" has chargeAt "${String(chargeAt)}"; use "afterGuards" (default) or "beforeGuards".`);
|
|
183
|
-
}
|
|
184
|
-
if (per === undefined || per === "ip" || per === "session") {
|
|
185
|
-
throw new Error(`Lambder: rate-limit policy "${name}" sets chargeAt, which only a policy keyed by a { apiInput?, handler } key takes: per "ip" and per "session" each run at one fixed place, and a policy without per is charged by the code that names it.`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
assertRateLimitMessage(`rate-limit policy "${name}"`, policy.refusal);
|
|
189
|
-
const budget = policy.budget;
|
|
190
|
-
if (budget !== undefined && budget !== "perApi" && budget !== "perPolicy") {
|
|
191
|
-
throw new Error(`Lambder: rate-limit policy "${name}" has budget "${String(budget)}"; use "perApi" (default: each referencing API counts separately) or "perPolicy" (one counter shared by every referencing API).`);
|
|
192
|
-
}
|
|
202
|
+
// Declaring the policies is declaring a limit, as for the guards
|
|
203
|
+
// option and an API's own `rateLimit: {}`. An empty map would
|
|
204
|
+
// configure a limiter with nothing to enforce, and every API naming a
|
|
205
|
+
// policy would be reported as if the option were missing: the wrong
|
|
206
|
+
// line to fix.
|
|
207
|
+
if (config.policies !== undefined && Object.keys(config.policies).length === 0) {
|
|
208
|
+
throw new Error("Lambder: the rateLimits option was declared with no policies in it, which configures nothing. Name the policies APIs will declare, or leave the policies out where the app's modules declare them all.");
|
|
193
209
|
}
|
|
210
|
+
for (const [name, policy] of Object.entries(config.policies ?? {}))
|
|
211
|
+
assertRateLimitPolicyDeclaration(name, policy);
|
|
194
212
|
const ipv6PrefixLength = config.ipv6PrefixLength ?? DEFAULT_IPV6_RATE_LIMIT_PREFIX;
|
|
195
213
|
if (!Number.isInteger(ipv6PrefixLength) || ipv6PrefixLength < 1 || ipv6PrefixLength > 128) {
|
|
196
214
|
throw new Error(`Lambder: rateLimits.ipv6PrefixLength must be a whole number from 1 to 128, got ${String(ipv6PrefixLength)}.`);
|
|
@@ -198,7 +216,24 @@ export class LambderApiRateLimitsEngine {
|
|
|
198
216
|
this.limiter = config.limiter;
|
|
199
217
|
this.failOpen = config.failOpen ?? true;
|
|
200
218
|
this.ipv6PrefixLength = ipv6PrefixLength;
|
|
201
|
-
this.policies = new Map(Object.entries(config.policies));
|
|
219
|
+
this.policies = new Map(Object.entries(config.policies ?? {}));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Adds a module's policies beside the ones declared so far, counted on
|
|
223
|
+
* the limiter given at creation. Its names were checked against them
|
|
224
|
+
* before (LambderApiPipeline.modulesToAdd), so a name met again here is a
|
|
225
|
+
* caller that skipped that check.
|
|
226
|
+
*/
|
|
227
|
+
add(policies) {
|
|
228
|
+
if (!this.limiter)
|
|
229
|
+
throw new Error("Lambder: rate-limit policies were added before a limiter was configured.");
|
|
230
|
+
for (const [name, policy] of Object.entries(policies)) {
|
|
231
|
+
if (this.policies.has(name))
|
|
232
|
+
throw new Error(`Lambder: rate-limit policy "${name}" is already defined.`);
|
|
233
|
+
assertRateLimitPolicyDeclaration(name, policy);
|
|
234
|
+
}
|
|
235
|
+
for (const [name, policy] of Object.entries(policies))
|
|
236
|
+
this.policies.set(name, policy);
|
|
202
237
|
}
|
|
203
238
|
/**
|
|
204
239
|
* Puts the engine over another limiter, for `lambder/testing`; the named
|
|
@@ -211,8 +246,12 @@ export class LambderApiRateLimitsEngine {
|
|
|
211
246
|
this.limiter = limiter;
|
|
212
247
|
return true;
|
|
213
248
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Startup validation of one API registration's rateLimit option.
|
|
251
|
+
* `adding` holds the policies of the modules the same registration adds,
|
|
252
|
+
* which the engine takes only once every endpoint of it passed.
|
|
253
|
+
*/
|
|
254
|
+
assertRegistration(apiName, mode, rateLimitOption, adding) {
|
|
216
255
|
const entries = toRateLimitEntries(rateLimitOption);
|
|
217
256
|
// Declaring the option is declaring a limit, as for guards: `{}`,
|
|
218
257
|
// `[]` and `{ name: undefined }` would register an API that
|
|
@@ -222,9 +261,9 @@ export class LambderApiRateLimitsEngine {
|
|
|
222
261
|
`Name the policy that limits it, or omit the option entirely.`);
|
|
223
262
|
}
|
|
224
263
|
for (const { name, override } of entries) {
|
|
225
|
-
const policy = this.policies.get(name);
|
|
264
|
+
const policy = this.policies.get(name) ?? (adding && Object.prototype.hasOwnProperty.call(adding, name) ? adding[name] : undefined);
|
|
226
265
|
if (!policy) {
|
|
227
|
-
throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation.`);
|
|
266
|
+
throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation, or, on a server, in the module the API is declared with (defineApiModule).`);
|
|
228
267
|
}
|
|
229
268
|
if (policy.per === undefined) {
|
|
230
269
|
throw new Error(`Lambder: API "${apiName}" references rate-limit policy "${name}", which declares no per: its key is the one a handler passes to ctx.rateLimit("${name}", key), so the request alone cannot be counted against it.`);
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -12,21 +12,21 @@ import { LambderFiles } from "./LambderFiles.js";
|
|
|
12
12
|
import { type LambderPipelineBackends, type LambderPipelineBackendSwap } from "../api/LambderApiPipeline.js";
|
|
13
13
|
import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
|
|
14
14
|
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
15
|
-
import { type
|
|
15
|
+
import { type LambderHandlerRefusalsOf, type LambderRefusalVocabularyOption, type LambderRefusalVocabularyOptionChecks, type LambderMergedRefusalVocabulary } from "../api/LambderApiRefusals.js";
|
|
16
16
|
import { type LambderDeclaredRefuse } from "../shared/wire/LambderApiRefusal.js";
|
|
17
17
|
import { type LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
|
|
18
18
|
import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
|
|
19
19
|
import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
|
|
20
|
-
import type {
|
|
21
|
-
import type {
|
|
22
|
-
import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
|
|
20
|
+
import type { LambderApiGuard, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
21
|
+
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
23
22
|
import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
|
|
24
|
-
import type
|
|
25
|
-
import { type
|
|
23
|
+
import { type LambderAppTypes, type LambderContractOfGroups, type LambderPlainAppTypes, type LambderRegisteredGroupsCheck, type LambderRegistrableApiGroup } from "../api/LambderApiDeclarations.js";
|
|
24
|
+
import { type LambderDefineApi, type LambderDefineApiGroup, type LambderDefineLazyApiGroup } from "./LambderApiBuilders.js";
|
|
25
|
+
import { LambderApiModule, type LambderApiModuleOptionChecks, type LambderApiModuleOptions, type LambderModuleAppTypes } from "./LambderApiModule.js";
|
|
26
26
|
import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
27
|
-
import type {
|
|
27
|
+
import type { LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
28
28
|
import { type LambderCallSummary } from "./LambderCallSummary.js";
|
|
29
|
-
import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type
|
|
29
|
+
import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderNoExtraKeys, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
|
|
30
30
|
/** Everything `lambder/testing` may put under a built instance: the pipeline's stores, and the source its files are read from. */
|
|
31
31
|
export type LambderInstanceBackends = LambderPipelineBackends & {
|
|
32
32
|
fileSource?: LambderFileSource;
|
|
@@ -44,17 +44,6 @@ export type LambderInstanceBackendSwap = LambderPipelineBackendSwap & {
|
|
|
44
44
|
* class cannot be read without it.
|
|
45
45
|
*/
|
|
46
46
|
export type LambderCreatedHook = (lambderInstance: Lambder<any, any>) => void | Promise<void>;
|
|
47
|
-
/** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
|
|
48
|
-
type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
|
|
49
|
-
/**
|
|
50
|
-
* What an endpoint's handler is handed: the session context when one of its
|
|
51
|
-
* guards needs a session (so ctx.session is typed present), the plain one
|
|
52
|
-
* otherwise, with the payload, the guards' data and a refuse() typed to the
|
|
53
|
-
* codes the endpoint may refuse with.
|
|
54
|
-
*/
|
|
55
|
-
type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt, TRefusalsOpt> = (LambderApiModeOf<TApp["guards"], TGuardsOpt> extends "session" ? LambderSessionRenderContext<TPayload, TApp["session"], Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["policies"]> : LambderRenderContext<TPayload, Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["session"], TApp["policies"]>) & {
|
|
56
|
-
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
|
|
57
|
-
};
|
|
58
47
|
/**
|
|
59
48
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
60
49
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
@@ -117,10 +106,15 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
117
106
|
private readonly apiGroupNames;
|
|
118
107
|
/** The lazy groups not loaded yet, each with its load in progress, if one is. */
|
|
119
108
|
private readonly lazyApiGroups;
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The guards given at creation merged into one map, and every registered
|
|
111
|
+
* module's beside them, kept for registration (a guard's session and
|
|
112
|
+
* refusal codes), apiSignatures() (a guard's schema is part of the
|
|
113
|
+
* signature of every endpoint declaring it) and apiOptionEntries().
|
|
114
|
+
*/
|
|
115
|
+
private guards;
|
|
116
|
+
/** The rate-limit policies given at creation, and every registered module's beside them, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
117
|
+
private rateLimitPolicies;
|
|
124
118
|
/** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
|
|
125
119
|
private readonly refusalVocabulary;
|
|
126
120
|
/** Every API's refusals option as written, for apiOptionEntries(); its definition holds the resolved set. */
|
|
@@ -150,7 +144,7 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
150
144
|
private readonly trustedClientIpHeaders;
|
|
151
145
|
private readonly trustedHostHeaders;
|
|
152
146
|
private readonly originProof;
|
|
153
|
-
/** Where each
|
|
147
|
+
/** Where each call's summary goes, an API call's or a route's (the callSummary option); null writes none. Replaceable through the backend swap alone. */
|
|
154
148
|
private callSummaryWriter;
|
|
155
149
|
constructor(given?: LambderCreateOptions<TApp["session"]>);
|
|
156
150
|
setRouteFallbackHandler(routeFallbackHandler: LambderFallbackHandler): this;
|
|
@@ -186,73 +180,50 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
186
180
|
* res.templateFile (markers optional) with no-cache.
|
|
187
181
|
*/
|
|
188
182
|
serveIndexHtml(handler?: LambderFallbackHandler, options?: LambderIndexHtmlOptions): this;
|
|
189
|
-
addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) =>
|
|
183
|
+
addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) => LambderMaybePromise<LambderResponse>): this;
|
|
190
184
|
addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
|
|
191
|
-
addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) =>
|
|
185
|
+
addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) => LambderMaybePromise<LambderResponse>) & LambderSessionEnabledInstance<TApp["sessions"]>): this;
|
|
192
186
|
addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TApp["session"]> & LambderSessionEnabledInstance<TApp["sessions"]>): this;
|
|
193
187
|
/**
|
|
194
|
-
* Declares one endpoint, typed on this instance's types
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
188
|
+
* Declares one endpoint, typed on this instance's types (see
|
|
189
|
+
* LambderDefineApi). Nothing is registered until the declaration is put
|
|
190
|
+
* in a group (defineApiGroup) and the group is registered
|
|
191
|
+
* (registerApiGroups).
|
|
198
192
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
193
|
+
* An arrow property rather than a method, as are the two below, so an
|
|
194
|
+
* app can hand them out detached; handed out under the instance's name
|
|
195
|
+
* (`export const defineApi: AppLambder["defineApi"] = lambderApp.defineApi`),
|
|
196
|
+
* a declaration file names each by the instance's type rather than
|
|
197
|
+
* writing the app's types out again in each.
|
|
198
|
+
*/
|
|
199
|
+
readonly defineApi: LambderDefineApi<TApp>;
|
|
200
|
+
/** Gathers endpoints into a named group (see LambderDefineApiGroup). */
|
|
201
|
+
readonly defineApiGroup: LambderDefineApiGroup;
|
|
202
|
+
/** A group loaded on the first call to one of its endpoints (see LambderDefineLazyApiGroup). */
|
|
203
|
+
readonly lazyApiGroup: LambderDefineLazyApiGroup;
|
|
204
|
+
/**
|
|
205
|
+
* Defines a module: guards and rate-limit policies a part of the app
|
|
206
|
+
* declares for its own endpoints, beside the ones this instance was
|
|
207
|
+
* created with. Returns the module's builders, whose endpoints may name
|
|
208
|
+
* the instance's guards and policies and the module's own, and nothing
|
|
209
|
+
* else; their groups are registered with registerApiGroups() as any
|
|
210
|
+
* are, and their registration adds the module's guards and policies to
|
|
211
|
+
* the instance that registers them.
|
|
204
212
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} & {
|
|
212
|
-
/** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. A per-session policy needs one of the endpoint's guards to need a session. */
|
|
213
|
-
rateLimit?: TRateOpt;
|
|
214
|
-
/** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
|
|
215
|
-
idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
|
|
216
|
-
/**
|
|
217
|
-
* The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
|
|
218
|
-
* non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
|
|
219
|
-
* its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
|
|
220
|
-
*/
|
|
221
|
-
refusals?: TRefusalsOpt;
|
|
222
|
-
/**
|
|
223
|
-
* Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
|
|
224
|
-
* body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
|
|
225
|
-
* compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
|
|
226
|
-
* cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
|
|
227
|
-
* both ends. A transport setting of this server's, not part of the API's contract.
|
|
228
|
-
*/
|
|
229
|
-
compress?: boolean | "auto";
|
|
230
|
-
} & LambderRequirableGuardsField<TApp["guardsRequired"], TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<TApp["guards"], TApp["policies"], TGuardsOpt, TRateOpt, z.input<TInput>> & LambderPublicSessionPolicyCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["policies"], TRateOpt> & LambderSessionModeCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["sessions"]>,
|
|
231
|
-
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
232
|
-
handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => MaybePromise<TAnswer>) => LambderApiDeclaration<LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, LambderApiModeOf<TApp["guards"], TGuardsOpt>, LambderGuardInputsOf<TApp["guards"], TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>>>;
|
|
233
|
-
/**
|
|
234
|
-
* Gathers endpoints into a named group: each is registered as
|
|
235
|
-
* `name.action` and called at `{apiPath}/{name}/{action}`, and every
|
|
236
|
-
* Lambder caller reaches it as `caller.name.action(input)`. A group's
|
|
237
|
-
* name and its actions are identifiers, and a group may not take a name
|
|
238
|
-
* a caller already answers for (see LAMBDER_RESERVED_GROUP_NAMES).
|
|
213
|
+
* ```typescript
|
|
214
|
+
* export const giftCardModule = storeApp.defineApiModule({
|
|
215
|
+
* guards: { cardHolder },
|
|
216
|
+
* rateLimitPolicies: { redeemPerCard: { perHour: 5, per: "session" } },
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
239
219
|
*
|
|
240
|
-
* A
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
220
|
+
* A name the instance declares is refused here, at compile time and at
|
|
221
|
+
* the call; a name two modules declare is refused when the second is
|
|
222
|
+
* registered, as create() refuses a name two maps declare. A module's
|
|
223
|
+
* rate-limit policies count on the instance's limiter, so they need the
|
|
224
|
+
* rateLimits option at creation, whose own policies may then be left out.
|
|
244
225
|
*/
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* A group loaded on the first call to one of its endpoints:
|
|
248
|
-
* `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
|
|
249
|
-
* A cold start then parses none of it, nor anything only it imports,
|
|
250
|
-
* until a request calls it; the contract is the loaded group's, read off
|
|
251
|
-
* its type. The group loaded must carry the same name, which the type
|
|
252
|
-
* checks and loading checks again. `loadApiGroups()` loads every group,
|
|
253
|
-
* for a build step or a boot check that has to see them all.
|
|
254
|
-
*/
|
|
255
|
-
readonly lazyApiGroup: <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
|
|
226
|
+
defineApiModule<const TOptions extends LambderApiModuleOptions<TApp["session"]>>(options: LambderNoExtraKeys<TOptions, LambderApiModuleOptions<TApp["session"]>> & LambderApiModuleOptionChecks<TApp, TOptions>): LambderApiModule<LambderModuleAppTypes<TApp, LambderGivenOption<TOptions, "guards">, LambderGivenOption<TOptions, "rateLimitPolicies">>>;
|
|
256
227
|
/**
|
|
257
228
|
* Registers groups of endpoints, as one contract: `typeof
|
|
258
229
|
* lambder.ApiContract` then maps every `group.action` to its entry, one
|
|
@@ -299,7 +270,12 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
299
270
|
* group answers each of them as an eager one does.
|
|
300
271
|
*/
|
|
301
272
|
private registeredDefinitionOf;
|
|
302
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* The endpoints of one group, each checked as `group.action` against the
|
|
275
|
+
* instance's guards and policies and those `adding` holds, of the modules
|
|
276
|
+
* the same registration adds; nothing is registered until they are
|
|
277
|
+
* committed.
|
|
278
|
+
*/
|
|
303
279
|
private prepareApiGroup;
|
|
304
280
|
/**
|
|
305
281
|
* A lazy group, loaded once: its import, checked to be the group it was
|
|
@@ -337,8 +313,8 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
337
313
|
* Use a type-guard filter to get a typed event:
|
|
338
314
|
* `(event): event is ScheduledEvent => ...`
|
|
339
315
|
*/
|
|
340
|
-
addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) =>
|
|
341
|
-
addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) =>
|
|
316
|
+
addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => LambderMaybePromise<unknown>): this;
|
|
317
|
+
addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => LambderMaybePromise<unknown>): this;
|
|
342
318
|
/**
|
|
343
319
|
* Hands the instance to a function that registers on it (routes, hooks,
|
|
344
320
|
* actions) and continues the chain with what it returns. Endpoints are
|
|
@@ -464,8 +440,8 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
464
440
|
private resolveRequest;
|
|
465
441
|
/**
|
|
466
442
|
* One HTTP invocation, from the event to the finalized response, under
|
|
467
|
-
* an invocation record of its own (see LambderInvocationScope): what
|
|
468
|
-
*
|
|
443
|
+
* an invocation record of its own (see LambderInvocationScope): what its
|
|
444
|
+
* call summary line is written from once the response is final.
|
|
469
445
|
*/
|
|
470
446
|
render(event: LambderHttpEvent, lambdaContext: Context): Promise<LambderHttpResponse>;
|
|
471
447
|
private renderRequest;
|
|
@@ -496,9 +472,9 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
496
472
|
*/
|
|
497
473
|
private answerCrash;
|
|
498
474
|
/**
|
|
499
|
-
* The summary line of an API call
|
|
500
|
-
* for an invocation that served
|
|
501
|
-
* the call its line and nothing else.
|
|
475
|
+
* The summary line of an API call or of a request a route answered, once
|
|
476
|
+
* its response is final: nothing for an invocation that served neither. A
|
|
477
|
+
* writer that throws costs the call its line and nothing else.
|
|
502
478
|
*/
|
|
503
479
|
private writeCallSummary;
|
|
504
480
|
/**
|
|
@@ -701,4 +677,3 @@ export declare const initLambder: <TSessionData = any>() => {
|
|
|
701
677
|
guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, {}, false>;
|
|
702
678
|
rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
|
|
703
679
|
};
|
|
704
|
-
export {};
|