lambder 3.6.1 → 3.8.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/Readme.md +31 -12
- package/dist/Lambder.d.ts +38 -15
- package/dist/Lambder.js +10 -5
- package/dist/LambderApiContract.d.ts +8 -2
- package/dist/LambderApiPolicies.d.ts +139 -22
- package/dist/LambderApiPolicies.js +42 -10
- package/dist/LambderCaller.d.ts +21 -3
- package/dist/LambderCaller.js +10 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lambder is a highly opinionated dynamic serverless framework designed to facilitate the management and implementation of routes and APIs within AWS Lambda functions, specifically tailored for TypeScript projects. It provides a streamlined approach to handling HTTP requests, managing sessions, and defining API routes, making serverless application development more intuitive and structured.
|
|
4
4
|
|
|
5
|
-
**New in v3:** Public file serving with `servePublicFiles()` + `serveIndexHtml()`, unified `addAction()` for non-HTTP triggers, automatic gzip + ETag, thrown responses with a real `die`, the comment-based `LambderTemplatingEngine`, type-safe `html`/`xml` tagged templates, API Gateway HTTP API (payload v2) / Lambda Function URL support, the `LambderDdbCache` DynamoDB cache (3.1), typed translations with `createLambderI18n` (3.2), and in 3.5: typed API refusals with `LambderApiError`, caller outcomes/timeouts with `apiOutcome()`, plus declarative per-API rate limits, guards, and idempotency.
|
|
5
|
+
**New in v3:** Public file serving with `servePublicFiles()` + `serveIndexHtml()`, unified `addAction()` for non-HTTP triggers, automatic gzip + ETag, thrown responses with a real `die`, the comment-based `LambderTemplatingEngine`, type-safe `html`/`xml` tagged templates, API Gateway HTTP API (payload v2) / Lambda Function URL support, the `LambderDdbCache` DynamoDB cache (3.1), typed translations with `createLambderI18n` (3.2), and in 3.5: typed API refusals with `LambderApiError`, caller outcomes/timeouts with `apiOutcome()`, plus declarative per-API rate limits, guards, and idempotency; 3.8 gives guards and custom rate-limit keys two typed modes: apiInput (checks a slice of the API's own payload; declarable only where the schema carries those fields) and guardInput (a separate client-sent guardInputs channel the contract makes mandatory at the call site).
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -466,7 +466,7 @@ Related: when an API call crashes with no `setGlobalErrorHandler` (or the handle
|
|
|
466
466
|
Declare named building blocks once; reference them from API definitions with full type inference (unknown names are compile errors, and everything is re-asserted at registration time for plain-JS safety). Each piece is independent and optional.
|
|
467
467
|
|
|
468
468
|
```typescript
|
|
469
|
-
import Lambder, { LambderDdbRateLimiter, LambderDdbIdempotency,
|
|
469
|
+
import Lambder, { LambderDdbRateLimiter, LambderDdbIdempotency, lambderGuard, lambderRateLimitKey, refuse } from "lambder";
|
|
470
470
|
|
|
471
471
|
const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
472
472
|
// 1. Rate limiting: your limiter instance + named policies. Each policy
|
|
@@ -476,8 +476,17 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
|
476
476
|
policies: {
|
|
477
477
|
authPerIp: { perMin: 5, perHour: 30, per: "ip" },
|
|
478
478
|
writePerUser: { perMin: 30, per: "session" }, // only referable from addSessionApi (also enforced at compile time)
|
|
479
|
-
codePerEmail: {
|
|
480
|
-
|
|
479
|
+
codePerEmail: {
|
|
480
|
+
perMin: 3,
|
|
481
|
+
// apiInput key: derives from the API's OWN payload. Validated
|
|
482
|
+
// before it runs, typed in the handler, and the policy is only
|
|
483
|
+
// referable from APIs whose input schema carries `email`.
|
|
484
|
+
per: lambderRateLimitKey({
|
|
485
|
+
apiInput: z.object({ email: z.string() }),
|
|
486
|
+
handler: (_ctx, { email }) => email.trim().toLowerCase(),
|
|
487
|
+
}),
|
|
488
|
+
errorMessage: { type: "warning", content: "Too many attempts for this address." },
|
|
489
|
+
},
|
|
481
490
|
},
|
|
482
491
|
})
|
|
483
492
|
// 2. Idempotency: a store instance + replay defaults. May share the rate
|
|
@@ -487,18 +496,27 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
|
487
496
|
defaultTtlSeconds: 24 * 3600,
|
|
488
497
|
failOpen: true, // DynamoDB down => execute without dedupe instead of failing
|
|
489
498
|
})
|
|
490
|
-
// 3. Named guards
|
|
491
|
-
//
|
|
499
|
+
// 3. Named guards, two modes. apiInput checks a slice of the API's own
|
|
500
|
+
// payload (the schema keeps the field; the guard is declarable only
|
|
501
|
+
// where the payload type passes both). guardInput is the guard's OWN
|
|
502
|
+
// value, sent separately by the caller via options.guardInputs and
|
|
503
|
+
// made mandatory by the contract, so forgetting it is a compile error
|
|
504
|
+
// at the call site. Both are validated pre-run and typed in the handler.
|
|
492
505
|
.defineApiGuards({
|
|
493
|
-
captcha:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
506
|
+
captcha: lambderGuard({
|
|
507
|
+
guardInput: z.object({ captchaToken: z.string() }),
|
|
508
|
+
handler: async (ctx, { captchaToken }) => {
|
|
509
|
+
if (!await verifyCaptcha(captchaToken, ctx.ip)) refuse("Verification failed, please retry.");
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
498
512
|
});
|
|
499
513
|
|
|
500
514
|
lambder.addApi("public.resetPassword", {
|
|
501
|
-
|
|
515
|
+
// captchaToken is NOT declared here: it travels in the separate
|
|
516
|
+
// guardInputs channel, so the guard validates and consumes it and the
|
|
517
|
+
// handler never sees it. `email` IS declared: the codePerEmail key runs
|
|
518
|
+
// in apiInput mode against the API's own payload.
|
|
519
|
+
input: z.object({ email: z.string().email() }),
|
|
502
520
|
output: z.object({ ok: z.boolean() }),
|
|
503
521
|
rateLimit: ["authPerIp", "codePerEmail"], // stacked: checked in order, first exceeded refuses (429 envelope)
|
|
504
522
|
guards: "captcha",
|
|
@@ -620,6 +638,7 @@ Also available:
|
|
|
620
638
|
|
|
621
639
|
- **Timeouts**: pass `timeoutMs` in the constructor for a default (API Gateway caps around 29s, so ~30000 is sensible) and/or per call; timed-out calls abort the fetch and report `reason: 'timeout'`. A per-call `signal` combines with the timeout.
|
|
622
640
|
- **Per-call handler overrides**: every constructor handler (`errorHandler`, `sessionExpiredHandler`, `errorMessageHandler`, ...) can be overridden in the options of a single `api`/`apiRaw`/`apiOutcome` call.
|
|
641
|
+
- **Guard inputs**: for APIs whose guards run in guardInput mode, pass their values per call as `guardInputs: { <guardName>: value }`; the typed contract makes the options argument (and the correct value shape) mandatory for those APIs.
|
|
623
642
|
- **Idempotency keys**: pass `idempotencyKey` per call for APIs declared idempotent on the server (see Declarative API Policies). Generate it once per logical operation with `LambderCaller.createIdempotencyKey()` (safe in insecure contexts where `crypto.randomUUID` is missing) and send the same key on retries; rotate after a confirmed success.
|
|
624
643
|
|
|
625
644
|
### Benefits
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { type LambderCorsConfig } from "./LambderCors.js";
|
|
|
8
8
|
import { type LambderSessionDataRefreshConfig } from "./LambderSessionManager.js";
|
|
9
9
|
import LambderSessionController, { type LambderSessionCookieOptions } from "./LambderSessionController.js";
|
|
10
10
|
import { type LambderPublicFilesOptions } from "./LambderPublicFiles.js";
|
|
11
|
-
import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig, type LambderApiIdempotencyConfig, type
|
|
11
|
+
import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig, type LambderApiIdempotencyConfig, type LambderApiGuard, type LambderGuardMetaMap, type LambderAllowedGuardNames, type LambderAllowedPolicyNames, type LambderGuardInputsOf } from "./LambderApiPolicies.js";
|
|
12
12
|
import type { MergeContract } from "./LambderApiContract.js";
|
|
13
13
|
import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
14
14
|
export type { PathParamsOf, RouteCondition, ConditionFunction, LambderRouteMatcher } from "./LambderRouting.js";
|
|
@@ -81,7 +81,7 @@ export type LambderConstructorOptions = {
|
|
|
81
81
|
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
82
82
|
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
83
83
|
* @typeParam _TRateLimitPolicies - @internal Accumulated by enableApiRateLimits (do not pass manually)
|
|
84
|
-
* @typeParam
|
|
84
|
+
* @typeParam _TGuards - @internal Guard name to required-payload map, accumulated by defineApiGuards (do not pass manually)
|
|
85
85
|
* @typeParam _TIdempotencyEnabled - @internal Flipped by enableApiIdempotency (do not pass manually)
|
|
86
86
|
*
|
|
87
87
|
* @example
|
|
@@ -93,7 +93,7 @@ export type LambderConstructorOptions = {
|
|
|
93
93
|
* .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
96
|
-
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}, _TRateLimitPolicies extends Record<string, LambderApiRateLimitPolicyConfig> = {},
|
|
96
|
+
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}, _TRateLimitPolicies extends Record<string, LambderApiRateLimitPolicyConfig> = {}, _TGuards extends Record<string, any> = {}, _TIdempotencyEnabled extends boolean = false> {
|
|
97
97
|
apiPath: string;
|
|
98
98
|
apiVersion: null | string;
|
|
99
99
|
publicPath: string;
|
|
@@ -188,7 +188,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
188
188
|
* policies keyed per "session" are only referable from addSessionApi.
|
|
189
189
|
* Callable once; call it before the API registrations that use it.
|
|
190
190
|
*/
|
|
191
|
-
enableApiRateLimits<const TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>>(config: LambderApiRateLimitsConfig<TPolicies>): Lambder<TSessionData, _TContract, TPolicies,
|
|
191
|
+
enableApiRateLimits<const TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>>(config: LambderApiRateLimitsConfig<TPolicies>): Lambder<TSessionData, _TContract, TPolicies, _TGuards, _TIdempotencyEnabled>;
|
|
192
192
|
/**
|
|
193
193
|
* Wire declarative idempotency: your LambderDdbIdempotency instance plus
|
|
194
194
|
* replay defaults. APIs opt in via `idempotency: true | { ttlSeconds }`;
|
|
@@ -198,15 +198,20 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
198
198
|
* 409, replays of a completed request return the stored response, and a
|
|
199
199
|
* crashed original releases its claim. Callable once.
|
|
200
200
|
*/
|
|
201
|
-
enableApiIdempotency(config: LambderApiIdempotencyConfig): Lambder<TSessionData, _TContract, _TRateLimitPolicies,
|
|
201
|
+
enableApiIdempotency(config: LambderApiIdempotencyConfig): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards, true>;
|
|
202
202
|
/**
|
|
203
203
|
* Define named guards that APIs reference (typed) via the `guards`
|
|
204
|
-
* option.
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
204
|
+
* option. Each guard is built with lambderGuard() in one of two modes:
|
|
205
|
+
* apiInput (checks a slice of the API's own payload; declarable only on
|
|
206
|
+
* APIs whose input schema carries those fields, so the payload type
|
|
207
|
+
* passes both the API input and the guard's apiInput) or guardInput (the
|
|
208
|
+
* client sends the guard's value separately via options.guardInputs, and
|
|
209
|
+
* the contract forces it at the call site). Guards run before input
|
|
210
|
+
* validation, in the order the API declares them; a handler refuses by
|
|
211
|
+
* throwing (typically refuse()). Callable multiple times so domain
|
|
212
|
+
* modules can contribute their own; names must not collide.
|
|
208
213
|
*/
|
|
209
|
-
defineApiGuards<TGuards extends Record<string,
|
|
214
|
+
defineApiGuards<TGuards extends Record<string, LambderApiGuard<any>>>(guards: TGuards): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards & LambderGuardMetaMap<TGuards>, _TIdempotencyEnabled>;
|
|
210
215
|
private getOrCreatePolicyEngine;
|
|
211
216
|
/** Registration-time checks shared by addApi/addSessionApi. */
|
|
212
217
|
private assertApiRegistration;
|
|
@@ -214,15 +219,33 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
214
219
|
addRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: ActionFunction): this;
|
|
215
220
|
addSessionRoute<TPath extends Path>(condition: TPath, actionFn: (ctx: LambderSessionRenderContext<any, TSessionData, PathParamsOf<TPath>>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
216
221
|
addSessionRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: SessionActionFunction<TSessionData>): this;
|
|
217
|
-
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract, any, any, any>) => Lambder<TSessionData, _TNewContract, any, any, any>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract), _TRateLimitPolicies,
|
|
218
|
-
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
222
|
+
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract, any, any, any>) => Lambder<TSessionData, _TNewContract, any, any, any>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract), _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled>;
|
|
223
|
+
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, const TRateOpt extends LambderAllowedPolicyNames<_TRateLimitPolicies, z.infer<TInput>, false> | readonly LambderAllowedPolicyNames<_TRateLimitPolicies, z.infer<TInput>, false>[] = never, const TGuardsOpt extends LambderAllowedGuardNames<_TGuards, z.infer<TInput>> | readonly LambderAllowedGuardNames<_TGuards, z.infer<TInput>>[] = never>(name: TName, schema: {
|
|
219
224
|
input: TInput;
|
|
220
225
|
output: TOutput;
|
|
221
|
-
} &
|
|
222
|
-
|
|
226
|
+
} & {
|
|
227
|
+
/** Named rate limits, checked in declared order before guards and validation; the first exceeded one refuses (429 envelope). */
|
|
228
|
+
rateLimit?: TRateOpt;
|
|
229
|
+
/** Named guards, run in declared order before input validation; their input requirements merge into this API's contract input. */
|
|
230
|
+
guards?: TGuardsOpt;
|
|
231
|
+
/** Replay-protect this API per client idempotencyKey. Requires enableApiIdempotency() first. */
|
|
232
|
+
idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
|
|
233
|
+
ttlSeconds?: number;
|
|
234
|
+
}) : never;
|
|
235
|
+
}, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>, LambderGuardInputsOf<_TGuards, TGuardsOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled>;
|
|
236
|
+
addSessionApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, const TRateOpt extends LambderAllowedPolicyNames<_TRateLimitPolicies, z.infer<TInput>, true> | readonly LambderAllowedPolicyNames<_TRateLimitPolicies, z.infer<TInput>, true>[] = never, const TGuardsOpt extends LambderAllowedGuardNames<_TGuards, z.infer<TInput>> | readonly LambderAllowedGuardNames<_TGuards, z.infer<TInput>>[] = never>(name: TName, schema: {
|
|
223
237
|
input: TInput;
|
|
224
238
|
output: TOutput;
|
|
225
|
-
} &
|
|
239
|
+
} & {
|
|
240
|
+
/** Named rate limits, checked in declared order before guards and validation; the first exceeded one refuses (429 envelope). */
|
|
241
|
+
rateLimit?: TRateOpt;
|
|
242
|
+
/** Named guards, run in declared order before input validation; their input requirements merge into this API's contract input. */
|
|
243
|
+
guards?: TGuardsOpt;
|
|
244
|
+
/** Replay-protect this API per client idempotencyKey. Requires enableApiIdempotency() first. */
|
|
245
|
+
idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
|
|
246
|
+
ttlSeconds?: number;
|
|
247
|
+
}) : never;
|
|
248
|
+
}, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>, LambderGuardInputsOf<_TGuards, TGuardsOpt>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled>;
|
|
226
249
|
/**
|
|
227
250
|
* Fetch the session or short-circuit the request: API calls get the
|
|
228
251
|
* protocol's { sessionExpired: true } response (handled by LambderCaller),
|
package/dist/Lambder.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
|
15
15
|
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
16
16
|
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
17
17
|
* @typeParam _TRateLimitPolicies - @internal Accumulated by enableApiRateLimits (do not pass manually)
|
|
18
|
-
* @typeParam
|
|
18
|
+
* @typeParam _TGuards - @internal Guard name to required-payload map, accumulated by defineApiGuards (do not pass manually)
|
|
19
19
|
* @typeParam _TIdempotencyEnabled - @internal Flipped by enableApiIdempotency (do not pass manually)
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
@@ -193,10 +193,15 @@ export default class Lambder {
|
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
195
|
* Define named guards that APIs reference (typed) via the `guards`
|
|
196
|
-
* option.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
196
|
+
* option. Each guard is built with lambderGuard() in one of two modes:
|
|
197
|
+
* apiInput (checks a slice of the API's own payload; declarable only on
|
|
198
|
+
* APIs whose input schema carries those fields, so the payload type
|
|
199
|
+
* passes both the API input and the guard's apiInput) or guardInput (the
|
|
200
|
+
* client sends the guard's value separately via options.guardInputs, and
|
|
201
|
+
* the contract forces it at the call site). Guards run before input
|
|
202
|
+
* validation, in the order the API declares them; a handler refuses by
|
|
203
|
+
* throwing (typically refuse()). Callable multiple times so domain
|
|
204
|
+
* modules can contribute their own; names must not collide.
|
|
200
205
|
*/
|
|
201
206
|
defineApiGuards(guards) {
|
|
202
207
|
this.getOrCreatePolicyEngine().addGuards(guards);
|
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
export type ApiContractShape = Record<string, {
|
|
10
10
|
input: any;
|
|
11
11
|
output: any;
|
|
12
|
+
/** Present when the API declares guardInput-mode guards: guard name -> value the client must send via options.guardInputs. */
|
|
13
|
+
guardInputs?: any;
|
|
12
14
|
}>;
|
|
13
15
|
/**
|
|
14
16
|
* Helper type for merging new API into existing contract during chaining
|
|
15
17
|
*/
|
|
16
|
-
export type MergeContract<Old, Name extends string, In, Out> = Old & {
|
|
17
|
-
[K in Name]: {
|
|
18
|
+
export type MergeContract<Old, Name extends string, In, Out, GuardInputs = never> = Old & {
|
|
19
|
+
[K in Name]: [GuardInputs] extends [never] ? {
|
|
18
20
|
input: In;
|
|
19
21
|
output: Out;
|
|
22
|
+
} : {
|
|
23
|
+
input: In;
|
|
24
|
+
output: Out;
|
|
25
|
+
guardInputs: GuardInputs;
|
|
20
26
|
};
|
|
21
27
|
};
|
|
@@ -1,14 +1,45 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
1
2
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
3
|
import type LambderResolver from "./LambderResolver.js";
|
|
3
4
|
import type { LambderRateLimitPolicy, LambderDdbRateLimiter } from "./LambderDdbRateLimiter.js";
|
|
4
5
|
import type { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
5
6
|
import { LambderResponse } from "./LambderResponse.js";
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* A custom rate-limit key. `apiInput` names the fields of the API's OWN
|
|
9
|
+
* payload the key derives from: the slice is validated against the raw
|
|
10
|
+
* payload before `handler` runs (failures answer the standard 422 validation
|
|
11
|
+
* shape) and the handler receives it typed. Referencing the policy from an
|
|
12
|
+
* API whose input schema does not carry those fields is a compile error, so
|
|
13
|
+
* the API's schema stays the single owner of the field. Build with
|
|
14
|
+
* lambderRateLimitKey() so the handler's payload type follows `apiInput`.
|
|
10
15
|
*/
|
|
11
|
-
export type
|
|
16
|
+
export type LambderRateLimitKeyFn<TInput extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
17
|
+
apiInput: TInput;
|
|
18
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
19
|
+
} | {
|
|
20
|
+
apiInput?: undefined;
|
|
21
|
+
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Builder that ties the handler's payload type to the `apiInput` schema
|
|
25
|
+
* inside one literal. Returns the exact union member so type extraction can
|
|
26
|
+
* see the schema.
|
|
27
|
+
*/
|
|
28
|
+
export declare function lambderRateLimitKey<TInput extends z.ZodTypeAny>(key: {
|
|
29
|
+
apiInput: TInput;
|
|
30
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
31
|
+
}): {
|
|
32
|
+
apiInput: TInput;
|
|
33
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
34
|
+
};
|
|
35
|
+
export declare function lambderRateLimitKey(key: {
|
|
36
|
+
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
37
|
+
}): {
|
|
38
|
+
apiInput?: undefined;
|
|
39
|
+
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
40
|
+
};
|
|
41
|
+
/** What one rate-limit counter tracks: the client IP, the session identity, or a custom payload-derived key. */
|
|
42
|
+
export type LambderRateLimitPer = "ip" | "session" | LambderRateLimitKeyFn<any>;
|
|
12
43
|
/** A named rate-limit policy: fixed windows plus the key one counter tracks. */
|
|
13
44
|
export type LambderApiRateLimitPolicyConfig = LambderRateLimitPolicy & {
|
|
14
45
|
per: LambderRateLimitPer;
|
|
@@ -30,28 +61,106 @@ export type LambderApiIdempotencyConfig = {
|
|
|
30
61
|
failOpen?: boolean;
|
|
31
62
|
};
|
|
32
63
|
/**
|
|
33
|
-
* A named guard, run before input validation.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
64
|
+
* A named guard, run before the API's own input validation. Two modes:
|
|
65
|
+
*
|
|
66
|
+
* - `apiInput`: the guard checks fields of the API's OWN payload. The slice
|
|
67
|
+
* is validated against the raw payload before `handler` runs and handed to
|
|
68
|
+
* it typed. The API's input schema stays the owner of those fields:
|
|
69
|
+
* declaring the guard on an API whose schema does not carry them is a
|
|
70
|
+
* compile error.
|
|
71
|
+
* - `guardInput`: the guard has its own value the client sends SEPARATELY,
|
|
72
|
+
* outside the API payload, via the caller's options.guardInputs[name].
|
|
73
|
+
* The requirement lands on the API's contract (`guardInputs`), so the
|
|
74
|
+
* typed caller refuses to compile a call that does not send it. The API
|
|
75
|
+
* payload and handler never see the value.
|
|
76
|
+
*
|
|
77
|
+
* Either way a validation failure answers the standard 422 shape, and the
|
|
78
|
+
* handler refuses by throwing (typically refuse()/LambderApiError). Build
|
|
79
|
+
* with lambderGuard() so the handler's payload type follows the schema.
|
|
36
80
|
*/
|
|
37
|
-
export type
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
export type LambderApiGuard<TInput extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
82
|
+
apiInput: TInput;
|
|
83
|
+
guardInput?: undefined;
|
|
84
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
85
|
+
} | {
|
|
86
|
+
guardInput: TInput;
|
|
87
|
+
apiInput?: undefined;
|
|
88
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
89
|
+
} | {
|
|
90
|
+
apiInput?: undefined;
|
|
91
|
+
guardInput?: undefined;
|
|
92
|
+
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Builder that ties the handler's payload type to the schema inside one
|
|
96
|
+
* literal. Returns the exact union member so mode/type extraction works.
|
|
97
|
+
*/
|
|
98
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny>(guard: {
|
|
99
|
+
apiInput: TInput;
|
|
100
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
101
|
+
}): {
|
|
102
|
+
apiInput: TInput;
|
|
103
|
+
guardInput?: undefined;
|
|
104
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
105
|
+
};
|
|
106
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny>(guard: {
|
|
107
|
+
guardInput: TInput;
|
|
108
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
109
|
+
}): {
|
|
110
|
+
guardInput: TInput;
|
|
111
|
+
apiInput?: undefined;
|
|
112
|
+
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
113
|
+
};
|
|
114
|
+
export declare function lambderGuard(guard: {
|
|
115
|
+
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
116
|
+
}): {
|
|
117
|
+
apiInput?: undefined;
|
|
118
|
+
guardInput?: undefined;
|
|
119
|
+
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
120
|
+
};
|
|
121
|
+
/** Per-guard metadata carried on the Lambder instance: mode plus payload type. */
|
|
122
|
+
export type LambderGuardMeta<G> = G extends {
|
|
123
|
+
apiInput: infer S extends z.ZodTypeAny;
|
|
124
|
+
} ? {
|
|
125
|
+
apiInput: z.output<S>;
|
|
126
|
+
} : G extends {
|
|
127
|
+
guardInput: infer S extends z.ZodTypeAny;
|
|
128
|
+
} ? {
|
|
129
|
+
guardInput: z.output<S>;
|
|
130
|
+
} : {};
|
|
131
|
+
export type LambderGuardMetaMap<TGuards> = {
|
|
132
|
+
[K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
|
|
133
|
+
};
|
|
134
|
+
type NamesIn<TOpt> = TOpt extends readonly (infer N extends string)[] ? N : TOpt extends string ? TOpt : never;
|
|
135
|
+
/** Guard names an API may declare: apiInput-mode guards only when the API's payload carries their fields. */
|
|
136
|
+
export type LambderAllowedGuardNames<TGuards, TPayload> = {
|
|
137
|
+
[K in keyof TGuards]: TGuards[K] extends {
|
|
138
|
+
apiInput: infer R;
|
|
139
|
+
} ? (TPayload extends R ? K : never) : K;
|
|
140
|
+
}[keyof TGuards] & string;
|
|
141
|
+
/**
|
|
142
|
+
* Policy names an API may reference: session-keyed policies only on session
|
|
143
|
+
* APIs, and apiInput-keyed policies only when the API's payload carries the
|
|
144
|
+
* key's fields.
|
|
145
|
+
*/
|
|
146
|
+
export type LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession extends boolean> = {
|
|
40
147
|
[K in keyof TPolicies]: TPolicies[K] extends {
|
|
41
148
|
per: "session";
|
|
42
|
-
} ? never : K
|
|
149
|
+
} ? (TIncludeSession extends true ? K : never) : TPolicies[K] extends {
|
|
150
|
+
per: {
|
|
151
|
+
apiInput: infer S extends z.ZodTypeAny;
|
|
152
|
+
};
|
|
153
|
+
} ? (TPayload extends z.output<S> ? K : never) : K;
|
|
43
154
|
}[keyof TPolicies] & string;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/** Replay-protect this API per client idempotencyKey. Requires enableApiIdempotency() first. */
|
|
51
|
-
idempotency?: TIdempotencyEnabled extends true ? (boolean | {
|
|
52
|
-
ttlSeconds?: number;
|
|
53
|
-
}) : never;
|
|
155
|
+
type GuardInputsEntries<TGuards, TOpt> = {
|
|
156
|
+
[K in Extract<NamesIn<TOpt>, keyof TGuards> as TGuards[K] extends {
|
|
157
|
+
guardInput: any;
|
|
158
|
+
} ? K : never]: TGuards[K] extends {
|
|
159
|
+
guardInput: infer V;
|
|
160
|
+
} ? V : never;
|
|
54
161
|
};
|
|
162
|
+
/** The guardInputs map an API's contract requires clients to send; never when no declared guard uses guardInput mode. */
|
|
163
|
+
export type LambderGuardInputsOf<TGuards, TOpt> = keyof GuardInputsEntries<TGuards, TOpt> extends never ? never : GuardInputsEntries<TGuards, TOpt>;
|
|
55
164
|
/**
|
|
56
165
|
* Runtime side of the declarative API options: holds what the enable/define
|
|
57
166
|
* calls declared, asserts registrations against it at startup, and executes
|
|
@@ -67,7 +176,7 @@ export declare class LambderApiPolicyEngine {
|
|
|
67
176
|
private idempotencyDefaultTtlSeconds;
|
|
68
177
|
private idempotencyFailOpen;
|
|
69
178
|
setRateLimits(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
70
|
-
addGuards(guards: Record<string,
|
|
179
|
+
addGuards(guards: Record<string, LambderApiGuard<any>>): void;
|
|
71
180
|
setIdempotency(config: LambderApiIdempotencyConfig): void;
|
|
72
181
|
/** Startup validation of one API registration's declarative options. */
|
|
73
182
|
assertRegistration(apiName: string, mode: "public" | "session", options: {
|
|
@@ -80,6 +189,13 @@ export declare class LambderApiPolicyEngine {
|
|
|
80
189
|
rateLimit?: string | readonly string[];
|
|
81
190
|
guards?: string | readonly string[];
|
|
82
191
|
}): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Validate a preflight input slice (an apiInput slice of the raw payload,
|
|
194
|
+
* or a guardInput value from the raw guardInputs map). Runs before the
|
|
195
|
+
* API's own validation; failures answer the same 422 shape as regular
|
|
196
|
+
* input validation.
|
|
197
|
+
*/
|
|
198
|
+
private parseSlice;
|
|
83
199
|
private resolveRateLimitKey;
|
|
84
200
|
/**
|
|
85
201
|
* Idempotency wrapper around validation-passed handler execution. Without
|
|
@@ -93,3 +209,4 @@ export declare class LambderApiPolicyEngine {
|
|
|
93
209
|
ttlSeconds?: number;
|
|
94
210
|
}, exec: () => Promise<LambderResponse>): Promise<LambderResponse>;
|
|
95
211
|
}
|
|
212
|
+
export {};
|
|
@@ -5,6 +5,8 @@ const IDEMPOTENCY_PENDING_TTL_SECONDS = 300;
|
|
|
5
5
|
/** Responses above this size skip replay storage (DynamoDB item limit is 400KB). */
|
|
6
6
|
const IDEMPOTENCY_MAX_STORED_BODY_BYTES = 350_000;
|
|
7
7
|
const RATE_LIMIT_WINDOW_KEYS = ["perMin", "per10Min", "perHour", "perDay", "perWeek", "perMonth"];
|
|
8
|
+
export function lambderRateLimitKey(key) { return key; }
|
|
9
|
+
export function lambderGuard(guard) { return guard; }
|
|
8
10
|
const toList = (value) => value === undefined ? [] : typeof value === "string" ? [value] : value;
|
|
9
11
|
/**
|
|
10
12
|
* Runtime side of the declarative API options: holds what the enable/define
|
|
@@ -24,8 +26,10 @@ export class LambderApiPolicyEngine {
|
|
|
24
26
|
if (this.limiter)
|
|
25
27
|
throw new Error("Lambder: enableApiRateLimits() was already called.");
|
|
26
28
|
for (const [name, policy] of Object.entries(config.policies)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const per = policy.per;
|
|
30
|
+
if (!per || (per !== "ip" && per !== "session" && typeof per.handler !== "function")) {
|
|
31
|
+
throw new Error(`Lambder: rate-limit policy "${name}" needs per: "ip", "session", or a { apiInput?, handler } key.`);
|
|
32
|
+
}
|
|
29
33
|
if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
|
|
30
34
|
throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
|
|
31
35
|
}
|
|
@@ -34,10 +38,14 @@ export class LambderApiPolicyEngine {
|
|
|
34
38
|
this.rateLimitPolicies = { ...config.policies };
|
|
35
39
|
}
|
|
36
40
|
addGuards(guards) {
|
|
37
|
-
for (const [name,
|
|
41
|
+
for (const [name, guardDef] of Object.entries(guards)) {
|
|
38
42
|
if (this.guards[name])
|
|
39
43
|
throw new Error(`Lambder: guard "${name}" is already defined.`);
|
|
40
|
-
|
|
44
|
+
if (typeof guardDef?.handler !== "function")
|
|
45
|
+
throw new Error(`Lambder: guard "${name}" has no handler function.`);
|
|
46
|
+
if (guardDef.apiInput && guardDef.guardInput)
|
|
47
|
+
throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
|
|
48
|
+
this.guards[name] = guardDef;
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
setIdempotency(config) {
|
|
@@ -73,7 +81,7 @@ export class LambderApiPolicyEngine {
|
|
|
73
81
|
const policy = this.rateLimitPolicies[name];
|
|
74
82
|
if (!policy || !this.limiter)
|
|
75
83
|
throw new Error(`Lambder: rate-limit policy "${name}" is not configured.`);
|
|
76
|
-
const key = await this.resolveRateLimitKey(ctx, policy.per);
|
|
84
|
+
const key = await this.resolveRateLimitKey(ctx, resolver, policy.per);
|
|
77
85
|
const limited = await this.limiter.isRateLimited(`api|${apiName}|${name}|${key}`, policy);
|
|
78
86
|
if (limited) {
|
|
79
87
|
throw new LambderApiError(`Rate limited: "${apiName}" exceeded policy "${name}".`, {
|
|
@@ -83,13 +91,34 @@ export class LambderApiPolicyEngine {
|
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
for (const name of toList(options.guards)) {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
94
|
+
const guardDef = this.guards[name];
|
|
95
|
+
if (!guardDef)
|
|
88
96
|
throw new Error(`Lambder: guard "${name}" is not configured.`);
|
|
89
|
-
|
|
97
|
+
const post = ctx.post;
|
|
98
|
+
let payload;
|
|
99
|
+
if (guardDef.apiInput) {
|
|
100
|
+
payload = this.parseSlice(guardDef.apiInput, post?.payload, resolver);
|
|
101
|
+
}
|
|
102
|
+
else if (guardDef.guardInput) {
|
|
103
|
+
payload = this.parseSlice(guardDef.guardInput, post?.guardInputs?.[name], resolver);
|
|
104
|
+
}
|
|
105
|
+
await guardDef.handler(ctx, payload, resolver);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Validate a preflight input slice (an apiInput slice of the raw payload,
|
|
110
|
+
* or a guardInput value from the raw guardInputs map). Runs before the
|
|
111
|
+
* API's own validation; failures answer the same 422 shape as regular
|
|
112
|
+
* input validation.
|
|
113
|
+
*/
|
|
114
|
+
parseSlice(input, value, resolver) {
|
|
115
|
+
const parsed = input.safeParse(value);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
throw resolver.json({ error: "Input validation failed", zodError: parsed.error }, { statusCode: 422 });
|
|
90
118
|
}
|
|
119
|
+
return parsed.data;
|
|
91
120
|
}
|
|
92
|
-
async resolveRateLimitKey(ctx, per) {
|
|
121
|
+
async resolveRateLimitKey(ctx, resolver, per) {
|
|
93
122
|
if (per === "ip")
|
|
94
123
|
return `ip:${ctx.ip}`;
|
|
95
124
|
if (per === "session") {
|
|
@@ -98,7 +127,10 @@ export class LambderApiPolicyEngine {
|
|
|
98
127
|
throw new Error('Lambder: rate-limit per "session" evaluated without a session on the context.');
|
|
99
128
|
return `session:${sessionKey}`;
|
|
100
129
|
}
|
|
101
|
-
|
|
130
|
+
const payload = per.apiInput
|
|
131
|
+
? this.parseSlice(per.apiInput, ctx.post?.payload, resolver)
|
|
132
|
+
: undefined;
|
|
133
|
+
return `custom:${await per.handler(ctx, payload)}`;
|
|
102
134
|
}
|
|
103
135
|
/**
|
|
104
136
|
* Idempotency wrapper around validation-passed handler execution. Without
|
package/dist/LambderCaller.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { LambderApiResponse } from './LambderResponseBuilder';
|
|
2
2
|
import type { ApiContractShape } from './LambderApiContract';
|
|
3
3
|
import type { z } from "zod";
|
|
4
|
+
type IsAny<T> = 0 extends (1 & T) ? true : false;
|
|
5
|
+
type GuardInputsOf<TEntry> = TEntry extends {
|
|
6
|
+
guardInputs: infer G;
|
|
7
|
+
} ? G : never;
|
|
8
|
+
/**
|
|
9
|
+
* The options argument: optional normally, REQUIRED (with guardInputs) when
|
|
10
|
+
* the API's contract declares guardInput-mode guards, so forgetting to send
|
|
11
|
+
* a guard's value is a compile error at the call site.
|
|
12
|
+
*/
|
|
13
|
+
type CallOptionsArg<TContract, TApiName> = IsAny<TContract> extends true ? [options?: LambderCallOptions] : TApiName extends keyof TContract ? [GuardInputsOf<TContract[TApiName]>] extends [never] ? [options?: LambderCallOptions] : [options: LambderCallOptions & {
|
|
14
|
+
guardInputs: GuardInputsOf<TContract[TApiName]>;
|
|
15
|
+
}] : [options?: LambderCallOptions];
|
|
4
16
|
type VoidFunction = () => void | Promise<void>;
|
|
5
17
|
type FetchTracker = {
|
|
6
18
|
apiName: string;
|
|
@@ -55,6 +67,12 @@ export type LambderCallOptions = {
|
|
|
55
67
|
timeoutMs?: number;
|
|
56
68
|
/** External abort signal, combined with the timeout when both are set. */
|
|
57
69
|
signal?: AbortSignal;
|
|
70
|
+
/**
|
|
71
|
+
* Values for the API's guardInput-mode guards, keyed by guard name; sent
|
|
72
|
+
* beside the payload and consumed by the guards before validation. The
|
|
73
|
+
* typed contract makes this REQUIRED for APIs that declare such guards.
|
|
74
|
+
*/
|
|
75
|
+
guardInputs?: Record<string, unknown>;
|
|
58
76
|
/**
|
|
59
77
|
* Replay-protection key for APIs declared idempotent on the server.
|
|
60
78
|
* Generate once per logical operation (e.g. crypto.randomUUID() when the
|
|
@@ -127,14 +145,14 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
|
127
145
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
128
146
|
* instead of collapsing every failure to null. Never throws.
|
|
129
147
|
*/
|
|
130
|
-
apiOutcome<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
148
|
+
apiOutcome<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: CallOptionsArg<TContract, TApiName>): Promise<LambderApiOutcome<TOutput>>;
|
|
131
149
|
/**
|
|
132
150
|
* Legacy shape: the parsed envelope on success (and on structured
|
|
133
151
|
* errorMessage refusals, which carry an envelope), null on every other
|
|
134
152
|
* failure. Prefer apiOutcome() when the call site needs to know why.
|
|
135
153
|
*/
|
|
136
|
-
apiRaw<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
154
|
+
apiRaw<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: CallOptionsArg<TContract, TApiName>): Promise<LambderApiResponse<TOutput> | null | undefined>;
|
|
137
155
|
/** Payload on success, null/undefined otherwise (indistinguishable from a null payload; prefer apiOutcome() when that matters). */
|
|
138
|
-
api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
156
|
+
api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: CallOptionsArg<TContract, TApiName>): Promise<TOutput | null | undefined>;
|
|
139
157
|
}
|
|
140
158
|
export {};
|
package/dist/LambderCaller.js
CHANGED
|
@@ -149,6 +149,7 @@ export default class LambderCaller {
|
|
|
149
149
|
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
150
150
|
body: JSON.stringify({
|
|
151
151
|
apiName, version, token, siteHost, payload,
|
|
152
|
+
...(options?.guardInputs !== undefined ? { guardInputs: options.guardInputs } : {}),
|
|
152
153
|
...(options?.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
153
154
|
}),
|
|
154
155
|
...(signal ? { signal } : {}),
|
|
@@ -280,8 +281,8 @@ export default class LambderCaller {
|
|
|
280
281
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
281
282
|
* instead of collapsing every failure to null. Never throws.
|
|
282
283
|
*/
|
|
283
|
-
async apiOutcome(apiName, payload,
|
|
284
|
-
return await this.dispatch(apiName, payload,
|
|
284
|
+
async apiOutcome(apiName, payload, ...rest) {
|
|
285
|
+
return await this.dispatch(apiName, payload, rest[0]);
|
|
285
286
|
}
|
|
286
287
|
;
|
|
287
288
|
/**
|
|
@@ -289,16 +290,18 @@ export default class LambderCaller {
|
|
|
289
290
|
* errorMessage refusals, which carry an envelope), null on every other
|
|
290
291
|
* failure. Prefer apiOutcome() when the call site needs to know why.
|
|
291
292
|
*/
|
|
292
|
-
async apiRaw(apiName, payload,
|
|
293
|
-
const outcome = await this.dispatch(apiName, payload,
|
|
293
|
+
async apiRaw(apiName, payload, ...rest) {
|
|
294
|
+
const outcome = await this.dispatch(apiName, payload, rest[0]);
|
|
294
295
|
if (outcome.ok)
|
|
295
296
|
return outcome.response;
|
|
296
297
|
return outcome.reason === 'errorMessage' ? outcome.response : null;
|
|
297
298
|
}
|
|
298
299
|
;
|
|
299
300
|
/** Payload on success, null/undefined otherwise (indistinguishable from a null payload; prefer apiOutcome() when that matters). */
|
|
300
|
-
async api(apiName, payload,
|
|
301
|
-
const
|
|
302
|
-
|
|
301
|
+
async api(apiName, payload, ...rest) {
|
|
302
|
+
const outcome = await this.dispatch(apiName, payload, rest[0]);
|
|
303
|
+
if (outcome.ok)
|
|
304
|
+
return outcome.response?.payload;
|
|
305
|
+
return outcome.reason === 'errorMessage' ? outcome.response?.payload : undefined;
|
|
303
306
|
}
|
|
304
307
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,8 @@ export { LambderDdbRateLimiter } from "./LambderDdbRateLimiter.js";
|
|
|
27
27
|
export type { LambderDdbRateLimiterOptions, LambderRateLimitPolicy, LambderRateLimitExceededMap, LambderRateLimitResult, } from "./LambderDdbRateLimiter.js";
|
|
28
28
|
export { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
29
29
|
export type { LambderDdbIdempotencyOptions, LambderIdempotencyBeginResult, } from "./LambderDdbIdempotency.js";
|
|
30
|
-
export
|
|
30
|
+
export { lambderGuard, lambderRateLimitKey } from "./LambderApiPolicies.js";
|
|
31
|
+
export type { LambderApiGuard, LambderRateLimitKeyFn, LambderRateLimitPer, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderApiIdempotencyConfig, LambderGuardMeta, LambderGuardMetaMap, LambderAllowedGuardNames, LambderAllowedPolicyNames, LambderGuardInputsOf, } from "./LambderApiPolicies.js";
|
|
31
32
|
export { createLambderI18n } from "./LambderI18n.js";
|
|
32
33
|
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./LambderI18n.js";
|
|
33
34
|
export { type ApiContractShape, } from "./LambderApiContract.js";
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export { LambderDdbCache } from "./LambderDdbCache.js";
|
|
|
23
23
|
export { LambderDdbRateLimiter } from "./LambderDdbRateLimiter.js";
|
|
24
24
|
// DynamoDB-backed idempotency records (standalone, server-only)
|
|
25
25
|
export { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
26
|
+
// Declarative per-API policies (rate limits, guards, idempotency)
|
|
27
|
+
export { lambderGuard, lambderRateLimitKey } from "./LambderApiPolicies.js";
|
|
26
28
|
// Typed translations (standalone, isomorphic)
|
|
27
29
|
export { createLambderI18n } from "./LambderI18n.js";
|
|
28
30
|
export { createContext, isV2HttpEvent } from "./LambderContext.js";
|