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 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, LambderApiError } from "lambder";
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: { perMin: 3, per: (ctx) => String(ctx.post?.payload?.email ?? "").toLowerCase(),
480
- errorMessage: { type: "warning", content: "Too many attempts for this address." } },
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: run before input validation, refuse by throwing.
491
- // Callable multiple times; domain modules can contribute their own.
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: async (ctx) => {
494
- if (!await verifyCaptcha(ctx.post?.payload?.captchaToken, ctx.ip)) {
495
- throw new LambderApiError("Captcha failed", { errorMessage: "Verification failed, please retry." });
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
- input: z.object({ email: z.string().email(), captchaToken: z.string() }),
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 LambderApiGuardFunction, type LambderApiRegistrationOptions, type LambderPublicRateLimitNames } from "./LambderApiPolicies.js";
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 _TGuardName - @internal Accumulated by defineApiGuards (do not pass manually)
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> = {}, _TGuardName extends string = never, _TIdempotencyEnabled extends boolean = false> {
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, _TGuardName, _TIdempotencyEnabled>;
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, _TGuardName, true>;
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. Guards run before input validation, in the order the API
205
- * declares them; a guard refuses by throwing (typically LambderApiError).
206
- * Callable multiple times so domain modules can contribute their own;
207
- * names must not collide.
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, LambderApiGuardFunction>>(guards: TGuards): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuardName | Extract<keyof TGuards, string>, _TIdempotencyEnabled>;
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, _TGuardName, _TIdempotencyEnabled>;
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
- } & LambderApiRegistrationOptions<LambderPublicRateLimitNames<_TRateLimitPolicies>, _TGuardName, _TIdempotencyEnabled>, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>, _TRateLimitPolicies, _TGuardName, _TIdempotencyEnabled>;
222
- addSessionApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
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
- } & LambderApiRegistrationOptions<Extract<keyof _TRateLimitPolicies, string>, _TGuardName, _TIdempotencyEnabled>, 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>>, _TRateLimitPolicies, _TGuardName, _TIdempotencyEnabled>;
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 _TGuardName - @internal Accumulated by defineApiGuards (do not pass manually)
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. Guards run before input validation, in the order the API
197
- * declares them; a guard refuses by throwing (typically LambderApiError).
198
- * Callable multiple times so domain modules can contribute their own;
199
- * names must not collide.
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
- * What one rate-limit counter tracks: the client IP, the session identity, or
8
- * a custom key derived from the request (e.g. a normalized email). Custom
9
- * functions run before input validation, so they read the raw payload.
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 LambderRateLimitPer = "ip" | "session" | ((ctx: LambderRenderContext) => string | Promise<string>);
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. Refuse by throwing (typically a
34
- * LambderApiError, or res.die.*); return normally to let the request through.
35
- * ctx.apiPayload is unvalidated at this point.
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 LambderApiGuardFunction = (ctx: LambderRenderContext, res: LambderResolver) => void | Promise<void>;
38
- /** Names of policies usable on public APIs: everything not keyed per "session". */
39
- export type LambderPublicRateLimitNames<TPolicies> = {
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
- /** Declarative per-API options carried in the addApi/addSessionApi schema object. */
45
- export type LambderApiRegistrationOptions<TRateLimitName extends string, TGuardName extends string, TIdempotencyEnabled extends boolean> = {
46
- /** Named rate limits, checked in declared order before guards and validation; the first exceeded one refuses (429 envelope). */
47
- rateLimit?: TRateLimitName | readonly TRateLimitName[];
48
- /** Named guards, run in declared order before input validation; refuse by throwing. */
49
- guards?: TGuardName | readonly TGuardName[];
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, LambderApiGuardFunction>): void;
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
- if (!policy.per)
28
- throw new Error(`Lambder: rate-limit policy "${name}" is missing its "per" key source.`);
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, guardFn] of Object.entries(guards)) {
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
- this.guards[name] = guardFn;
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 guardFn = this.guards[name];
87
- if (!guardFn)
94
+ const guardDef = this.guards[name];
95
+ if (!guardDef)
88
96
  throw new Error(`Lambder: guard "${name}" is not configured.`);
89
- await guardFn(ctx, resolver);
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
- return `custom:${await per(ctx)}`;
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
@@ -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, options?: LambderCallOptions): Promise<LambderApiOutcome<TOutput>>;
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, options?: LambderCallOptions): Promise<LambderApiResponse<TOutput> | null | undefined>;
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, options?: LambderCallOptions): Promise<TOutput | null | undefined>;
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 {};
@@ -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, options) {
284
- return await this.dispatch(apiName, payload, options);
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, options) {
293
- const outcome = await this.dispatch(apiName, payload, options);
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, options) {
301
- const result = await this.apiRaw(apiName, payload, options);
302
- return result?.payload;
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 type { LambderApiGuardFunction, LambderRateLimitPer, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderApiIdempotencyConfig, LambderApiRegistrationOptions, LambderPublicRateLimitNames, } from "./LambderApiPolicies.js";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "3.6.1",
3
+ "version": "3.8.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",