lambder 3.7.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 +17 -14
- package/dist/Lambder.d.ts +15 -14
- package/dist/Lambder.js +9 -8
- package/dist/LambderApiContract.d.ts +8 -2
- package/dist/LambderApiPolicies.d.ts +97 -62
- package/dist/LambderApiPolicies.js +20 -11
- package/dist/LambderCaller.d.ts +21 -3
- package/dist/LambderCaller.js +10 -7
- package/dist/index.d.ts +1 -1
- 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; 3.
|
|
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
|
|
|
@@ -478,11 +478,11 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
|
478
478
|
writePerUser: { perMin: 30, per: "session" }, // only referable from addSessionApi (also enforced at compile time)
|
|
479
479
|
codePerEmail: {
|
|
480
480
|
perMin: 3,
|
|
481
|
-
//
|
|
482
|
-
// it runs,
|
|
483
|
-
//
|
|
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
484
|
per: lambderRateLimitKey({
|
|
485
|
-
|
|
485
|
+
apiInput: z.object({ email: z.string() }),
|
|
486
486
|
handler: (_ctx, { email }) => email.trim().toLowerCase(),
|
|
487
487
|
}),
|
|
488
488
|
errorMessage: { type: "warning", content: "Too many attempts for this address." },
|
|
@@ -496,14 +496,15 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
|
496
496
|
defaultTtlSeconds: 24 * 3600,
|
|
497
497
|
failOpen: true, // DynamoDB down => execute without dedupe instead of failing
|
|
498
498
|
})
|
|
499
|
-
// 3. Named guards
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
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.
|
|
504
505
|
.defineApiGuards({
|
|
505
506
|
captcha: lambderGuard({
|
|
506
|
-
|
|
507
|
+
guardInput: z.object({ captchaToken: z.string() }),
|
|
507
508
|
handler: async (ctx, { captchaToken }) => {
|
|
508
509
|
if (!await verifyCaptcha(captchaToken, ctx.ip)) refuse("Verification failed, please retry.");
|
|
509
510
|
},
|
|
@@ -511,9 +512,10 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
|
|
|
511
512
|
});
|
|
512
513
|
|
|
513
514
|
lambder.addApi("public.resetPassword", {
|
|
514
|
-
// captchaToken is NOT declared here:
|
|
515
|
-
//
|
|
516
|
-
// sees it.
|
|
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.
|
|
517
519
|
input: z.object({ email: z.string().email() }),
|
|
518
520
|
output: z.object({ ok: z.boolean() }),
|
|
519
521
|
rateLimit: ["authPerIp", "codePerEmail"], // stacked: checked in order, first exceeded refuses (429 envelope)
|
|
@@ -636,6 +638,7 @@ Also available:
|
|
|
636
638
|
|
|
637
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.
|
|
638
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.
|
|
639
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.
|
|
640
643
|
|
|
641
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 LambderApiGuard, 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";
|
|
@@ -201,16 +201,17 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
201
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. Each guard is
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
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.
|
|
212
213
|
*/
|
|
213
|
-
defineApiGuards<TGuards extends Record<string, LambderApiGuard<any>>>(guards: TGuards): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards &
|
|
214
|
+
defineApiGuards<TGuards extends Record<string, LambderApiGuard<any>>>(guards: TGuards): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards & LambderGuardMetaMap<TGuards>, _TIdempotencyEnabled>;
|
|
214
215
|
private getOrCreatePolicyEngine;
|
|
215
216
|
/** Registration-time checks shared by addApi/addSessionApi. */
|
|
216
217
|
private assertApiRegistration;
|
|
@@ -219,7 +220,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
219
220
|
addSessionRoute<TPath extends Path>(condition: TPath, actionFn: (ctx: LambderSessionRenderContext<any, TSessionData, PathParamsOf<TPath>>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
220
221
|
addSessionRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: SessionActionFunction<TSessionData>): this;
|
|
221
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>;
|
|
222
|
-
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, const TRateOpt extends
|
|
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: {
|
|
223
224
|
input: TInput;
|
|
224
225
|
output: TOutput;
|
|
225
226
|
} & {
|
|
@@ -231,8 +232,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
231
232
|
idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
|
|
232
233
|
ttlSeconds?: number;
|
|
233
234
|
}) : never;
|
|
234
|
-
}, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName,
|
|
235
|
-
addSessionApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, const TRateOpt extends
|
|
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: {
|
|
236
237
|
input: TInput;
|
|
237
238
|
output: TOutput;
|
|
238
239
|
} & {
|
|
@@ -244,7 +245,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
244
245
|
idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
|
|
245
246
|
ttlSeconds?: number;
|
|
246
247
|
}) : never;
|
|
247
|
-
}, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName,
|
|
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>;
|
|
248
249
|
/**
|
|
249
250
|
* Fetch the session or short-circuit the request: API calls get the
|
|
250
251
|
* protocol's { sessionExpired: true } response (handled by LambderCaller),
|
package/dist/Lambder.js
CHANGED
|
@@ -193,14 +193,15 @@ export default class Lambder {
|
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
195
|
* Define named guards that APIs reference (typed) via the `guards`
|
|
196
|
-
* option. Each guard is
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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.
|
|
204
205
|
*/
|
|
205
206
|
defineApiGuards(guards) {
|
|
206
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
|
};
|
|
@@ -5,36 +5,37 @@ import type { LambderRateLimitPolicy, LambderDdbRateLimiter } from "./LambderDdb
|
|
|
5
5
|
import type { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
6
6
|
import { LambderResponse } from "./LambderResponse.js";
|
|
7
7
|
/**
|
|
8
|
-
* A custom rate-limit key
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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`.
|
|
14
15
|
*/
|
|
15
16
|
export type LambderRateLimitKeyFn<TInput extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
16
|
-
|
|
17
|
+
apiInput: TInput;
|
|
17
18
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
18
19
|
} | {
|
|
19
|
-
|
|
20
|
+
apiInput?: undefined;
|
|
20
21
|
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
|
-
* Builder that ties the handler's payload type to the `
|
|
24
|
-
* one literal. Returns the exact union member
|
|
25
|
-
*
|
|
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.
|
|
26
27
|
*/
|
|
27
28
|
export declare function lambderRateLimitKey<TInput extends z.ZodTypeAny>(key: {
|
|
28
|
-
|
|
29
|
+
apiInput: TInput;
|
|
29
30
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
30
31
|
}): {
|
|
31
|
-
|
|
32
|
+
apiInput: TInput;
|
|
32
33
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>) => string | Promise<string>;
|
|
33
34
|
};
|
|
34
35
|
export declare function lambderRateLimitKey(key: {
|
|
35
36
|
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
36
37
|
}): {
|
|
37
|
-
|
|
38
|
+
apiInput?: undefined;
|
|
38
39
|
handler: (ctx: LambderRenderContext, payload: undefined) => string | Promise<string>;
|
|
39
40
|
};
|
|
40
41
|
/** What one rate-limit counter tracks: the client IP, the session identity, or a custom payload-derived key. */
|
|
@@ -60,72 +61,106 @@ export type LambderApiIdempotencyConfig = {
|
|
|
60
61
|
failOpen?: boolean;
|
|
61
62
|
};
|
|
62
63
|
/**
|
|
63
|
-
* A named guard, run before the API's own input validation.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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.
|
|
71
80
|
*/
|
|
72
81
|
export type LambderApiGuard<TInput extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
73
|
-
|
|
82
|
+
apiInput: TInput;
|
|
83
|
+
guardInput?: undefined;
|
|
74
84
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
75
85
|
} | {
|
|
76
|
-
|
|
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;
|
|
77
92
|
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
78
93
|
};
|
|
79
94
|
/**
|
|
80
|
-
* Builder that ties the handler's payload type to the
|
|
81
|
-
*
|
|
82
|
-
* extraction can see the `input` type.
|
|
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.
|
|
83
97
|
*/
|
|
84
98
|
export declare function lambderGuard<TInput extends z.ZodTypeAny>(guard: {
|
|
85
|
-
|
|
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;
|
|
86
108
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
87
109
|
}): {
|
|
88
|
-
|
|
110
|
+
guardInput: TInput;
|
|
111
|
+
apiInput?: undefined;
|
|
89
112
|
handler: (ctx: LambderRenderContext, payload: z.output<TInput>, res: LambderResolver) => void | Promise<void>;
|
|
90
113
|
};
|
|
91
114
|
export declare function lambderGuard(guard: {
|
|
92
115
|
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
93
116
|
}): {
|
|
94
|
-
|
|
117
|
+
apiInput?: undefined;
|
|
118
|
+
guardInput?: undefined;
|
|
95
119
|
handler: (ctx: LambderRenderContext, payload: undefined, res: LambderResolver) => void | Promise<void>;
|
|
96
120
|
};
|
|
97
|
-
/**
|
|
98
|
-
export type
|
|
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> = {
|
|
99
147
|
[K in keyof TPolicies]: TPolicies[K] extends {
|
|
100
148
|
per: "session";
|
|
101
|
-
} ? 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;
|
|
102
154
|
}[keyof TPolicies] & string;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} ?
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
[K in keyof TGuards]: LambderGuardPayload<TGuards[K]>;
|
|
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;
|
|
110
161
|
};
|
|
111
|
-
/**
|
|
112
|
-
export type
|
|
113
|
-
per: {
|
|
114
|
-
input: infer S extends z.ZodTypeAny;
|
|
115
|
-
};
|
|
116
|
-
} ? z.output<S> : {};
|
|
117
|
-
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
118
|
-
type NamesIn<TOpt> = TOpt extends readonly (infer N extends string)[] ? N : TOpt extends string ? TOpt : never;
|
|
119
|
-
/** Intersection of the payload requirements of the referenced guards; never when none are declared. */
|
|
120
|
-
export type LambderGuardsRequirement<TGuardPayloads, TOpt> = [
|
|
121
|
-
NamesIn<TOpt>
|
|
122
|
-
] extends [never] ? never : UnionToIntersection<TGuardPayloads[Extract<NamesIn<TOpt>, keyof TGuardPayloads>]>;
|
|
123
|
-
/** Intersection of the payload requirements of the referenced rate-limit policies; never when none are declared. */
|
|
124
|
-
export type LambderPoliciesRequirement<TPolicies, TOpt> = [
|
|
125
|
-
NamesIn<TOpt>
|
|
126
|
-
] extends [never] ? never : UnionToIntersection<LambderPolicyPayload<TPolicies[Extract<NamesIn<TOpt>, keyof TPolicies>]>>;
|
|
127
|
-
/** Contract-input merge: the API's own input plus everything its rate limits and guards force clients to send. */
|
|
128
|
-
export type LambderMergedInput<TIn, TReqA, TReqB> = ([TReqA] extends [never] ? TIn : TIn & TReqA) extends infer TMid ? ([TReqB] extends [never] ? TMid : TMid & TReqB) : never;
|
|
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>;
|
|
129
164
|
/**
|
|
130
165
|
* Runtime side of the declarative API options: holds what the enable/define
|
|
131
166
|
* calls declared, asserts registrations against it at startup, and executes
|
|
@@ -155,10 +190,10 @@ export declare class LambderApiPolicyEngine {
|
|
|
155
190
|
guards?: string | readonly string[];
|
|
156
191
|
}): Promise<void>;
|
|
157
192
|
/**
|
|
158
|
-
* Validate a preflight input slice
|
|
159
|
-
*
|
|
160
|
-
* API
|
|
161
|
-
*
|
|
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.
|
|
162
197
|
*/
|
|
163
198
|
private parseSlice;
|
|
164
199
|
private resolveRateLimitKey;
|
|
@@ -28,7 +28,7 @@ export class LambderApiPolicyEngine {
|
|
|
28
28
|
for (const [name, policy] of Object.entries(config.policies)) {
|
|
29
29
|
const per = policy.per;
|
|
30
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 {
|
|
31
|
+
throw new Error(`Lambder: rate-limit policy "${name}" needs per: "ip", "session", or a { apiInput?, handler } key.`);
|
|
32
32
|
}
|
|
33
33
|
if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
|
|
34
34
|
throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
|
|
@@ -43,6 +43,8 @@ export class LambderApiPolicyEngine {
|
|
|
43
43
|
throw new Error(`Lambder: guard "${name}" is already defined.`);
|
|
44
44
|
if (typeof guardDef?.handler !== "function")
|
|
45
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.`);
|
|
46
48
|
this.guards[name] = guardDef;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -92,20 +94,25 @@ export class LambderApiPolicyEngine {
|
|
|
92
94
|
const guardDef = this.guards[name];
|
|
93
95
|
if (!guardDef)
|
|
94
96
|
throw new Error(`Lambder: guard "${name}" is not configured.`);
|
|
95
|
-
const
|
|
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
|
+
}
|
|
96
105
|
await guardDef.handler(ctx, payload, resolver);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
/**
|
|
100
|
-
* Validate a preflight input slice
|
|
101
|
-
*
|
|
102
|
-
* API
|
|
103
|
-
*
|
|
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.
|
|
104
113
|
*/
|
|
105
|
-
parseSlice(input,
|
|
106
|
-
|
|
107
|
-
return undefined;
|
|
108
|
-
const parsed = input.safeParse(ctx.post?.payload);
|
|
114
|
+
parseSlice(input, value, resolver) {
|
|
115
|
+
const parsed = input.safeParse(value);
|
|
109
116
|
if (!parsed.success) {
|
|
110
117
|
throw resolver.json({ error: "Input validation failed", zodError: parsed.error }, { statusCode: 422 });
|
|
111
118
|
}
|
|
@@ -120,7 +127,9 @@ export class LambderApiPolicyEngine {
|
|
|
120
127
|
throw new Error('Lambder: rate-limit per "session" evaluated without a session on the context.');
|
|
121
128
|
return `session:${sessionKey}`;
|
|
122
129
|
}
|
|
123
|
-
const payload =
|
|
130
|
+
const payload = per.apiInput
|
|
131
|
+
? this.parseSlice(per.apiInput, ctx.post?.payload, resolver)
|
|
132
|
+
: undefined;
|
|
124
133
|
return `custom:${await per.handler(ctx, payload)}`;
|
|
125
134
|
}
|
|
126
135
|
/**
|
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
|
@@ -28,7 +28,7 @@ export type { LambderDdbRateLimiterOptions, LambderRateLimitPolicy, LambderRateL
|
|
|
28
28
|
export { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
29
29
|
export type { LambderDdbIdempotencyOptions, LambderIdempotencyBeginResult, } from "./LambderDdbIdempotency.js";
|
|
30
30
|
export { lambderGuard, lambderRateLimitKey } from "./LambderApiPolicies.js";
|
|
31
|
-
export type { LambderApiGuard, LambderRateLimitKeyFn, LambderRateLimitPer, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderApiIdempotencyConfig,
|
|
31
|
+
export type { LambderApiGuard, LambderRateLimitKeyFn, LambderRateLimitPer, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderApiIdempotencyConfig, LambderGuardMeta, LambderGuardMetaMap, LambderAllowedGuardNames, LambderAllowedPolicyNames, LambderGuardInputsOf, } from "./LambderApiPolicies.js";
|
|
32
32
|
export { createLambderI18n } from "./LambderI18n.js";
|
|
33
33
|
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./LambderI18n.js";
|
|
34
34
|
export { type ApiContractShape, } from "./LambderApiContract.js";
|