lambder 4.0.1 → 4.1.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
@@ -8,9 +8,9 @@ Lambder is a highly opinionated dynamic serverless framework designed to facilit
8
8
  - **Hardened policy layer**: rate-limit policies can share one counter across APIs (`scope: "policy"`); idempotency replays answer before rate limits, survive client IP changes (key-scoped for public APIs, 16-char minimum keys), store full response headers, refuse to store Set-Cookie responses, and Brotli-compress stored bodies of 1KB+ so the ~350KB replay budget applies to compressed bytes.
9
9
  - **Secrets hashed at rest**: session records store only sha256 hashes of the bearer secrets, so a session-table read yields no usable cookies; `LambderSessionReadError` keeps a DynamoDB blip from reading as a logout.
10
10
  - **Three package entry points**: `lambder` (server), `lambder/client` (browser-safe by construction: no AWS SDK, no Node built-ins), `lambder/testing` (`LambderMSW`); sources organized into core/policies/session/stores/client/shared.
11
- - **Quality of life**: the `LambderApp` alias for annotating api modules, `LambderCaller.createIdempotencyKeyScope()` for one self-rotating key per logical operation, fail-open rate limiting logs its passes.
11
+ - **Configuration at creation**: `initLambder<SessionData>().create({...})` takes the WHOLE configuration (serving options, session, cors, rate limits, guards, idempotency) in one declaration; the enable/define chain methods are gone, so nothing can be half-configured or wired in the wrong order, and api modules annotate with `typeof lambderApp` derived from the real instance. Plus `LambderCaller.createIdempotencyKeyScope()` for one self-rotating key per logical operation, and fail-open rate limiting logs its passes.
12
12
 
13
- **Breaking in v4** (from 3.x): session records are reshaped (hashes at rest; live sessions invalidate once on upgrade, clients just re-login) and the manager-level `createSession`/`regenerateSession` return `LambderCreatedSession` (`{ session, sessionToken, csrfToken }`; the controller API is unchanged); `LambderMSW` moved from the root entry to `lambder/testing`; `LambderCaller.apiRaw()` is removed (use `apiOutcome()`, whose failure outcomes carry the envelope on `response`); the `multiValueHeaders` alias on `res.raw()` is removed (use `headers`); `LambderDdbIdempotency.complete()` answers `"stored" | "too-large" | "lost"`; idempotency keys must be 16-200 chars.
13
+ **Breaking in v4** (from 3.x): configuration moved entirely to creation, removing `enableCors`, `enableDdbSession`, `setSessionCookieKey`, `enableApiRateLimits`, `enableApiIdempotency`, and `defineApiGuards` in favor of the `cors`/`session`/`rateLimits`/`guards`/`idempotency` options of `initLambder().create({...})`; session records are reshaped (hashes at rest; live sessions invalidate once on upgrade, clients just re-login) and the manager-level `createSession`/`regenerateSession` return `LambderCreatedSession` (`{ session, sessionToken, csrfToken }`; the controller API is unchanged); `LambderMSW` moved from the root entry to `lambder/testing`; `LambderCaller.apiRaw()` is removed (use `apiOutcome()`, whose failure outcomes carry the envelope on `response`); the `multiValueHeaders` alias on `res.raw()` is removed (use `headers`); `LambderDdbIdempotency.complete()` answers `"stored" | "too-large" | "lost"`; idempotency keys must be 16-200 chars.
14
14
 
15
15
  v3 (public file serving, `addAction()`, gzip + ETag, thrown responses, `LambderTemplatingEngine`, `html`/`xml` tags, payload v2 support, `LambderDdbCache`, `createLambderI18n`, `LambderApiError`/`refuse()`, `apiOutcome()`, the declarative policy foundations) is documented in the git history.
16
16
 
@@ -63,25 +63,32 @@ Source layout mirrors this: `src/core/` (request pipeline), `src/policies/` (dec
63
63
 
64
64
  ### Basic Setup
65
65
 
66
+ The whole configuration is given at creation, in one declaration; only
67
+ registration (routes, apis, hooks, `use()`) chains afterwards. `initLambder`
68
+ is curried so the session data type is fixed first and everything else
69
+ (policy names, guard metadata) is INFERRED from the options; TypeScript type
70
+ arguments are all-or-nothing per call, so a plain `new Lambder<SessionData>({...})`
71
+ would silently widen the inferred policy types, which is why the curried
72
+ creator is the canonical entry.
73
+
66
74
  ```typescript
67
- import Lambder from 'lambder';
75
+ import { initLambder } from 'lambder';
68
76
  import { z } from 'zod';
69
77
  import * as path from 'path';
70
78
 
71
- const lambder = new Lambder({
79
+ interface SessionData { userId: string; }
80
+
81
+ const lambder = initLambder<SessionData>().create({
72
82
  apiPath: "/api",
73
83
  publicPath: path.resolve(`./public`),
74
- });
75
-
76
- // Enable session and CORS
77
- lambder
78
- .enableDdbSession({
84
+ session: {
79
85
  tableName: "website-session",
80
86
  tableRegion: "us-east-1",
81
- sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING"
82
- })
87
+ sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
88
+ },
83
89
  // true allows any origin; or configure: { origins: ["https://app.example.com"], credentials: true }
84
- .enableCors(true);
90
+ cors: true,
91
+ });
85
92
 
86
93
  // Define type-safe APIs with Zod schemas
87
94
  lambder
@@ -277,18 +284,21 @@ lambder
277
284
 
278
285
  ### Session Management
279
286
 
280
- Enable DynamoDB-based sessions with `enableDdbSession()`. Optional configuration:
287
+ Enable DynamoDB-based sessions with the `session` option at creation:
281
288
 
282
289
  ```typescript
283
- lambder
284
- .enableDdbSession({
290
+ const lambder = initLambder<SessionData>().create({
291
+ apiPath: "/api",
292
+ session: {
285
293
  tableName: "website-session",
286
294
  tableRegion: "us-east-1",
287
295
  sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
288
- enableSlidingExpiration: true // Optional: extend session on each access
289
- })
290
- // Optionally customize session cookie names (defaults: LMDRSESSIONTKID, LMDRSESSIONCSTK)
291
- .setSessionCookieKey("MY_SESSION_TOKEN", "MY_CSRF_TOKEN");
296
+ enableSlidingExpiration: true, // Optional: extend session on each access
297
+ // Optionally customize cookie names (defaults: LMDRSESSIONTKID, LMDRSESSIONCSTK)
298
+ tokenCookieKey: "MY_SESSION_TOKEN",
299
+ csrfCookieKey: "MY_CSRF_TOKEN",
300
+ },
301
+ });
292
302
  ```
293
303
 
294
304
  #### DynamoDB Session Table Structure
@@ -308,16 +318,18 @@ The session cookie is `pkHash:secret`: `pkHash = sha256(sessionKey + sessionSalt
308
318
  Session data often caches values derived from external state: roles, permissions, feature flags. Opt in to `dataRefresh` to give that data a shelf life. Every session read checks it, and once `ttlSeconds` have passed your `refresh` callback rebuilds the data, which is persisted onto the same session record: same tokens, same cookies, the session itself is untouched. Changes to the source of truth then reach every live session within `ttlSeconds`, with no mass session invalidation.
309
319
 
310
320
  ```typescript
311
- lambder.enableDdbSession({
312
- tableName: "website-session",
313
- tableRegion: "us-east-1",
314
- sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
315
- dataRefresh: {
316
- ttlSeconds: 600, // data is renewed at most every 10 minutes
317
- refresh: async (session) => {
318
- const user = await loadUser(session.data.userId);
319
- if (!user || user.disabled) return null; // null ends the session
320
- return buildSessionData(user);
321
+ const lambder = initLambder<SessionData>().create({
322
+ session: {
323
+ tableName: "website-session",
324
+ tableRegion: "us-east-1",
325
+ sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
326
+ dataRefresh: {
327
+ ttlSeconds: 600, // data is renewed at most every 10 minutes
328
+ refresh: async (session) => {
329
+ const user = await loadUser(session.data.userId);
330
+ if (!user || user.disabled) return null; // null ends the session
331
+ return buildSessionData(user);
332
+ },
321
333
  },
322
334
  },
323
335
  });
@@ -495,12 +507,13 @@ Related: when an API call crashes with no `setGlobalErrorHandler` (or the handle
495
507
  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.
496
508
 
497
509
  ```typescript
498
- import Lambder, { LambderDdbRateLimiter, LambderDdbIdempotency, lambderGuard, lambderRateLimitKey, refuse } from "lambder";
510
+ import { initLambder, LambderDdbRateLimiter, LambderDdbIdempotency, lambderGuard, lambderRateLimitKey, refuse } from "lambder";
499
511
 
500
- const lambder = new Lambder<SessionData>({ apiPath: "/api" })
512
+ const lambder = initLambder<SessionData>().create({
513
+ apiPath: "/api",
501
514
  // 1. Rate limiting: your limiter instance + named policies. Each policy
502
515
  // declares its windows AND what one counter tracks ("per").
503
- .enableApiRateLimits({
516
+ rateLimits: {
504
517
  limiter: new LambderDdbRateLimiter({ tableName: "app-rate-limiter", region: "us-east-1", failOpen: true }),
505
518
  policies: {
506
519
  authPerIp: { perMin: 5, perHour: 30, per: "ip" },
@@ -521,14 +534,14 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
521
534
  errorMessage: { type: "warning", content: "Too many attempts for this address." },
522
535
  },
523
536
  },
524
- })
537
+ },
525
538
  // 2. Idempotency: a store instance + replay defaults. May share the rate
526
539
  // limiter's table (records use an IDEM# key prefix).
527
- .enableApiIdempotency({
540
+ idempotency: {
528
541
  store: new LambderDdbIdempotency({ tableName: "app-rate-limiter", region: "us-east-1" }),
529
542
  defaultTtlSeconds: 24 * 3600,
530
543
  failOpen: true, // DynamoDB down => execute without dedupe instead of failing
531
- })
544
+ },
532
545
  // 3. Named guards. Input modes: apiInput checks a slice of the API's own
533
546
  // payload (the schema keeps the field; the guard is declarable only
534
547
  // where the payload type passes both); guardInput is the guard's OWN
@@ -539,7 +552,7 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
539
552
  // (session: true, declarable only on addSessionApi), take a per-API
540
553
  // PARAM (annotate a 4th handler argument), and RETURN a value that
541
554
  // lands typed on the API handler's ctx.guardData[name].
542
- .defineApiGuards({
555
+ guards: {
543
556
  captcha: lambderGuard({
544
557
  guardInput: z.object({ captchaToken: z.string() }),
545
558
  handler: async (ctx, { captchaToken }) => {
@@ -557,7 +570,8 @@ const lambder = new Lambder<SessionData>({ apiPath: "/api" })
557
570
  handler: (ctx, _payload, _res, permission: PermissionString) =>
558
571
  requirePermissionOrRefuse(ctx.session, permission), // return value → ctx.guardData.orgPermission
559
572
  }),
560
- });
573
+ },
574
+ });
561
575
 
562
576
  lambder.addApi("public.resetPassword", {
563
577
  // captchaToken is NOT declared here: it travels in the separate
@@ -575,7 +589,7 @@ lambder.addSessionApi("secure.order.create", {
575
589
  output: OrderResultSchema,
576
590
  rateLimit: "writePerUser",
577
591
  guards: { orgPermission: "ORDERS.CREATE" }, // param typed per guard; entries run in insertion order
578
- idempotency: true, // or { ttlSeconds: 3600 }; type error until enableApiIdempotency()
592
+ idempotency: true, // or { ttlSeconds: 3600 }; type error unless created with idempotency
579
593
  }, async (ctx, res) => {
580
594
  const { organizationId } = ctx.guardData.orgPermission; // typed guard output
581
595
  // ...
@@ -584,15 +598,25 @@ lambder.addSessionApi("secure.order.create", {
584
598
 
585
599
  Guard results are typed end to end: the handler's `ctx.guardData` carries exactly the declared guards that return a value, a session guard on a public API is a compile error (and a startup assert), an apiInput guard is declarable only where the API's schema carries its fields, and a parameterized guard's param is typechecked in the declaration.
586
600
 
587
- For api modules split across files, annotate their `lambder` parameter with the `LambderApp` alias instead of hand-writing the instance generics:
601
+ For api modules split across files, DERIVE the annotation type from the real instance instead of writing it by hand: create the instance next to the policy declarations and export `typeof` it. The type can never drift from what actually runs, and modules import it without a cycle (the app file imports no modules):
588
602
 
589
603
  ```typescript
590
- export type AppLambder = LambderApp<SessionData, {
591
- policies: typeof apiRateLimitPolicies; // what enableApiRateLimits({ policies }) receives
592
- guards: typeof apiGuards; // what defineApiGuards(...) receives
593
- idempotency: true; // enableApiIdempotency(...) is called
594
- }>;
604
+ // app.ts: declarations + the fully configured instance
605
+ export const lambderApp = initLambder<SessionData>().create({
606
+ apiPath: "/api",
607
+ session: { tableName: "app-session", tableRegion: "us-east-1", sessionSalt: "..." },
608
+ rateLimits: { limiter, policies: apiRateLimitPolicies },
609
+ idempotency: { store: idempotencyStore },
610
+ guards: apiGuards,
611
+ });
612
+ export type AppLambder = typeof lambderApp;
613
+
614
+ // orders.ts: an api module
595
615
  export const orderApi = (lambder: AppLambder) => lambder.addSessionApi(...);
616
+
617
+ // index.ts: registration only (hooks, routes, modules)
618
+ const lambder = lambderApp.addHook(...).use(orderApi)...;
619
+ export const handler = lambder.getHandler();
596
620
  ```
597
621
 
598
622
  Request flow per API: session (session APIs) → idempotency replay lookup → rate limits → guards → zod validation → idempotency claim → handler → idempotency store. The replay lookup runs first on purpose: a completed idempotent request answers its stored response without burning rate-limit quota or re-running guards (the original already passed them, and no handler executes either way). Refusals ride the envelope via `LambderApiError` (429 rate limited, 409 duplicate in flight), so the caller's `errorMessageHandler` surfaces them with zero client code.
@@ -64,7 +64,40 @@ export type LambderHandler = {
64
64
  (event: LambderHttpEvent, context: Context): Promise<LambderHttpResponse>;
65
65
  (event: unknown, context: Context): Promise<unknown>;
66
66
  };
67
- export type LambderConstructorOptions = {
67
+ /** DynamoDB session configuration (the `session` option of create/new). */
68
+ export type LambderSessionOptions<TSessionData = any> = {
69
+ tableName: string;
70
+ tableRegion: string;
71
+ sessionSalt: string;
72
+ enableSlidingExpiration?: boolean;
73
+ /** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
74
+ slidingWriteIntervalSeconds?: number;
75
+ /** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. `domain` may be a (hostname) => string function for multi-domain deployments. */
76
+ cookie?: LambderSessionCookieOptions;
77
+ /** Session cookie names. Defaults: LMDRSESSIONTKID / LMDRSESSIONCSTK. */
78
+ tokenCookieKey?: string;
79
+ csrfCookieKey?: string;
80
+ partitionKey?: string;
81
+ sortKey?: string;
82
+ /**
83
+ * Opt-in freshness for session.data derived from external state (roles,
84
+ * permissions, feature flags...). Every session read renews data past
85
+ * its ttlSeconds via your refresh callback, persisting in place on the
86
+ * same record: same tokens, same cookies. Return null from refresh to
87
+ * end the session. See LambderSessionDataRefreshConfig for the exact
88
+ * semantics.
89
+ */
90
+ dataRefresh?: LambderSessionDataRefreshConfig<TSessionData>;
91
+ };
92
+ /**
93
+ * Everything an instance is configured with, in ONE declaration: base
94
+ * serving options plus the type-affecting policy layer (rate limits,
95
+ * guards, idempotency) and session/CORS config. There are no enable/define
96
+ * chain methods; the instance is born fully configured and fully typed
97
+ * (via initLambder), so no ordering rules exist and no partially-configured
98
+ * instance type ever needs a name.
99
+ */
100
+ export type LambderCreateOptions<TSessionData = any> = {
68
101
  publicPath?: string;
69
102
  apiPath?: string;
70
103
  apiVersion?: string;
@@ -76,21 +109,34 @@ export type LambderConstructorOptions = {
76
109
  etag?: boolean;
77
110
  /** Guard threshold for Lambda's ~6MB response cap. Default: 5,500,000. */
78
111
  maxResponseBytes?: number;
112
+ /** CORS: true allows any origin; or pass a LambderCorsConfig. Default: off. */
113
+ cors?: boolean | LambderCorsConfig;
114
+ /** DynamoDB-backed sessions; required for addSessionApi/addSessionRoute. */
115
+ session?: LambderSessionOptions<TSessionData>;
116
+ /** Declarative per-API rate limiting: your limiter plus named policies APIs reference (typed) via the `rateLimit` option. */
117
+ rateLimits?: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>;
118
+ /** Named guards APIs reference (typed) via the `guards` option; build each with lambderGuard(). */
119
+ guards?: Record<string, LambderApiGuard<any, any, any>>;
120
+ /** Declarative idempotency: your store plus replay defaults; APIs opt in via `idempotency: true | { ttlSeconds }`. */
121
+ idempotency?: LambderApiIdempotencyConfig;
79
122
  };
80
123
  /**
81
- * Main Lambder class for building type-safe serverless APIs
124
+ * Main Lambder class for building type-safe serverless APIs. Create
125
+ * instances with initLambder<SessionData>().create({...}) (see below): the
126
+ * whole configuration, including the typed policy layer, is given at
127
+ * construction, and only registration (routes, apis, hooks, use) chains.
82
128
  *
83
129
  * @typeParam TSessionData - Type of session data stored in DynamoDB
84
130
  * @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
85
- * @typeParam _TRateLimitPolicies - @internal Accumulated by enableApiRateLimits (do not pass manually)
86
- * @typeParam _TGuards - @internal Guard name to required-payload map, accumulated by defineApiGuards (do not pass manually)
87
- * @typeParam _TIdempotencyEnabled - @internal Flipped by enableApiIdempotency (do not pass manually)
131
+ * @typeParam _TRateLimitPolicies - @internal Inferred from create()'s rateLimits.policies (do not pass manually)
132
+ * @typeParam _TGuards - @internal Guard metadata map inferred from create()'s guards (do not pass manually)
133
+ * @typeParam _TIdempotencyEnabled - @internal True when create() received idempotency (do not pass manually)
88
134
  *
89
135
  * @example
90
136
  * ```typescript
91
137
  * interface SessionData { userId: string; role: string; }
92
138
  *
93
- * const lambder = new Lambder<SessionData>({ apiPath: '/api' })
139
+ * const lambder = initLambder<SessionData>().create({ apiPath: '/api' })
94
140
  * .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
95
141
  * .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
96
142
  * ```
@@ -130,30 +176,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
130
176
  private sessionCookieOptions;
131
177
  private sessionTokenCookieKey;
132
178
  private sessionCsrfCookieKey;
133
- constructor(options?: LambderConstructorOptions);
134
- enableCors(config: boolean | LambderCorsConfig): this;
135
- enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds, cookie, partitionKey, sortKey, dataRefresh, }: {
136
- tableName: string;
137
- tableRegion: string;
138
- sessionSalt: string;
139
- enableSlidingExpiration?: boolean;
140
- /** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
141
- slidingWriteIntervalSeconds?: number;
142
- /** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. `domain` may be a (hostname) => string function for multi-domain deployments. */
143
- cookie?: LambderSessionCookieOptions;
144
- partitionKey?: string;
145
- sortKey?: string;
146
- /**
147
- * Opt-in freshness for session.data derived from external state
148
- * (roles, permissions, feature flags...). Every session read
149
- * renews data past its ttlSeconds via your refresh callback,
150
- * persisting in place on the same record: same tokens, same
151
- * cookies. Return null from refresh to end the session. See
152
- * LambderSessionDataRefreshConfig for the exact semantics.
153
- */
154
- dataRefresh?: LambderSessionDataRefreshConfig<TSessionData>;
155
- }): this;
156
- setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this;
179
+ constructor(options?: LambderCreateOptions<TSessionData>);
157
180
  setRouteFallbackHandler(routeFallbackHandler: FallbackHandlerFunction): this;
158
181
  setApiFallbackHandler(apiFallbackHandler: FallbackHandlerFunction): this;
159
182
  setApiInputValidationErrorHandler(apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction): this;
@@ -181,39 +204,6 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
181
204
  serveIndexHtml(handler?: FallbackHandlerFunction, options?: LambderIndexHtmlOptions): this;
182
205
  /** Apply the serveIndexHtml gates; null means fall through. */
183
206
  private tryServeIndexHtml;
184
- /**
185
- * Wire declarative per-API rate limiting: your LambderDdbRateLimiter
186
- * instance plus named policies, each declaring its windows and what one
187
- * counter tracks (`per`: "ip", "session", or a custom key function).
188
- * APIs then reference policies by name via the `rateLimit` option; the
189
- * returned type narrows so only declared names are accepted, and
190
- * policies keyed per "session" are only referable from addSessionApi.
191
- * Callable once; call it before the API registrations that use it.
192
- */
193
- enableApiRateLimits<const TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>>(config: LambderApiRateLimitsConfig<TPolicies>): Lambder<TSessionData, _TContract, TPolicies, _TGuards, _TIdempotencyEnabled>;
194
- /**
195
- * Wire declarative idempotency: your LambderDdbIdempotency instance plus
196
- * replay defaults. APIs opt in via `idempotency: true | { ttlSeconds }`;
197
- * the option is a type error until this is called. Requests carrying a
198
- * client `idempotencyKey` (sent by LambderCaller) claim an
199
- * identity+api+key scope atomically: concurrent duplicates refuse with
200
- * 409, replays of a completed request return the stored response, and a
201
- * crashed original releases its claim. Callable once.
202
- */
203
- enableApiIdempotency(config: LambderApiIdempotencyConfig): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards, true>;
204
- /**
205
- * Define named guards that APIs reference (typed) via the `guards`
206
- * option. Each guard is built with lambderGuard(): its input mode
207
- * (apiInput slice of the API's own payload, a separate client-sent
208
- * guardInput, or none), an optional `session: true` requirement, an
209
- * optional parameter APIs pass in their declaration (`guards: { name:
210
- * param }`), and an optional return value that lands typed on the
211
- * handler's ctx.guardData[name]. Guards run before input validation, in
212
- * the order the API declares them; a handler refuses by throwing
213
- * (typically refuse()). Callable multiple times so domain modules can
214
- * contribute their own; names must not collide.
215
- */
216
- defineApiGuards<TGuards extends Record<string, LambderApiGuard<any, any, any>>>(guards: TGuards): Lambder<TSessionData, _TContract, _TRateLimitPolicies, _TGuards & LambderGuardMetaMap<TGuards>, _TIdempotencyEnabled>;
217
207
  private getOrCreatePolicyEngine;
218
208
  /** Registration-time checks shared by addApi/addSessionApi. */
219
209
  private assertApiRegistration;
@@ -230,7 +220,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
230
220
  rateLimit?: TRateOpt;
231
221
  /** Named guards, run in declared order before input validation: a name, a list of names, or a { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
232
222
  guards?: TGuardsOpt;
233
- /** Replay-protect this API per client idempotencyKey. Requires enableApiIdempotency() first. */
223
+ /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
234
224
  idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
235
225
  ttlSeconds?: number;
236
226
  }) : never;
@@ -243,7 +233,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
243
233
  rateLimit?: TRateOpt;
244
234
  /** Named guards, run in declared order before input validation: a name, a list of names, or a { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
245
235
  guards?: TGuardsOpt;
246
- /** Replay-protect this API per client idempotencyKey. Requires enableApiIdempotency() first. */
236
+ /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
247
237
  idempotency?: _TIdempotencyEnabled extends true ? (boolean | {
248
238
  ttlSeconds?: number;
249
239
  }) : never;
@@ -293,25 +283,42 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
293
283
  render(event: LambderHttpEvent, lambdaContext: Context): Promise<LambderHttpResponse>;
294
284
  }
295
285
  /**
296
- * The app's configured Lambder instance type, for annotating the parameter
297
- * of api modules used via lambder.use(...). Name the pieces you wired in
298
- * index.ts and the guard metadata mapping happens for you:
286
+ * The canonical way to create an instance: fix the session data type first,
287
+ * then create with the full configuration in one declaration; the policy,
288
+ * guard, and idempotency types are INFERRED from the options, so the
289
+ * instance is born fully typed and `typeof lambderApp` is the annotation
290
+ * type for api modules. No enable/define chain exists, so there are no
291
+ * ordering rules and nothing can be half-configured.
299
292
  *
300
293
  * ```typescript
301
- * export type AppLambder = LambderApp<SessionData, {
302
- * policies: typeof apiRateLimitPolicies; // enableApiRateLimits({ policies })
303
- * guards: typeof apiGuards; // defineApiGuards(apiGuards)
304
- * idempotency: true; // enableApiIdempotency(...) was called
305
- * }>;
294
+ * // app.ts (imports no api modules, so modules can import the type back)
295
+ * export const lambderApp = initLambder<SessionData>().create({
296
+ * apiPath: "/api",
297
+ * session: { tableName: "app-session", tableRegion: "us-east-1", sessionSalt: "..." },
298
+ * rateLimits: { limiter, policies },
299
+ * guards,
300
+ * idempotency: { store },
301
+ * });
302
+ * export type AppLambder = typeof lambderApp;
303
+ *
304
+ * // orders.ts
305
+ * export const orderApi = (lambder: AppLambder) => lambder.addSessionApi(...);
306
+ *
307
+ * // index.ts: registration only
308
+ * const lambder = lambderApp.addHook(...).use(orderApi)...;
309
+ * export const handler = lambder.getHandler();
306
310
  * ```
307
311
  *
308
- * Every field is optional; omit what the app does not wire. The declaration
309
- * is still an assertion about index.ts (registration-time asserts backstop a
310
- * mismatch at cold start), but derive the fields from the same exported
311
- * consts the enable calls receive and the types cannot drift.
312
+ * Why curried (`initLambder<S>().create(...)` rather than
313
+ * `new Lambder<S>(...)`): TypeScript type arguments are all-or-nothing per
314
+ * call, so explicitly passing the session data type to the constructor
315
+ * would silently WIDEN the inferred policy and guard types to their {}
316
+ * defaults. Fixing the session type in the first call lets the second call
317
+ * infer everything else from the options. `new Lambder(options)` remains
318
+ * for untyped or session-data-free instances.
312
319
  */
313
- export type LambderApp<TSessionData, TConfig extends {
314
- policies?: Record<string, LambderApiRateLimitPolicyConfig>;
315
- guards?: Record<string, LambderApiGuard<any, any, any>>;
316
- idempotency?: boolean;
317
- } = {}> = Lambder<TSessionData, {}, TConfig["policies"] extends Record<string, LambderApiRateLimitPolicyConfig> ? TConfig["policies"] : {}, TConfig["guards"] extends Record<string, LambderApiGuard<any, any, any>> ? LambderGuardMetaMap<TConfig["guards"]> : {}, TConfig["idempotency"] extends true ? true : false>;
320
+ export declare const initLambder: <TSessionData = any>() => {
321
+ create<const TOptions extends LambderCreateOptions<TSessionData>>(options: TOptions): Lambder<TSessionData, {}, TOptions["rateLimits"] extends {
322
+ policies: infer TPolicies extends Record<string, LambderApiRateLimitPolicyConfig>;
323
+ } ? TPolicies : {}, TOptions["guards"] extends Record<string, LambderApiGuard<any, any, any>> ? LambderGuardMetaMap<TOptions["guards"]> : {}, TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false>;
324
+ };
@@ -10,19 +10,22 @@ import { isLambderApiError } from "../shared/LambderApiError.js";
10
10
  import { LambderApiPolicyEngine } from "../policies/LambderApiPolicies.js";
11
11
  import { createContext, isV2HttpEvent } from "./LambderContext.js";
12
12
  /**
13
- * Main Lambder class for building type-safe serverless APIs
13
+ * Main Lambder class for building type-safe serverless APIs. Create
14
+ * instances with initLambder<SessionData>().create({...}) (see below): the
15
+ * whole configuration, including the typed policy layer, is given at
16
+ * construction, and only registration (routes, apis, hooks, use) chains.
14
17
  *
15
18
  * @typeParam TSessionData - Type of session data stored in DynamoDB
16
19
  * @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
17
- * @typeParam _TRateLimitPolicies - @internal Accumulated by enableApiRateLimits (do not pass manually)
18
- * @typeParam _TGuards - @internal Guard name to required-payload map, accumulated by defineApiGuards (do not pass manually)
19
- * @typeParam _TIdempotencyEnabled - @internal Flipped by enableApiIdempotency (do not pass manually)
20
+ * @typeParam _TRateLimitPolicies - @internal Inferred from create()'s rateLimits.policies (do not pass manually)
21
+ * @typeParam _TGuards - @internal Guard metadata map inferred from create()'s guards (do not pass manually)
22
+ * @typeParam _TIdempotencyEnabled - @internal True when create() received idempotency (do not pass manually)
20
23
  *
21
24
  * @example
22
25
  * ```typescript
23
26
  * interface SessionData { userId: string; role: string; }
24
27
  *
25
- * const lambder = new Lambder<SessionData>({ apiPath: '/api' })
28
+ * const lambder = initLambder<SessionData>().create({ apiPath: '/api' })
26
29
  * .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
27
30
  * .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
28
31
  * ```
@@ -73,26 +76,33 @@ export default class Lambder {
73
76
  etag: options.etag ?? DEFAULT_FINALIZE_OPTIONS.etag,
74
77
  maxResponseBytes: options.maxResponseBytes ?? DEFAULT_FINALIZE_OPTIONS.maxResponseBytes,
75
78
  };
76
- }
77
- enableCors(config) {
78
- this.corsConfig = config === true ? {} : (config === false ? null : config);
79
- return this;
80
- }
81
- enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds, cookie, partitionKey, sortKey, dataRefresh, }) {
82
- this.lambderSessionManager = new LambderSessionManager({
83
- tableName, tableRegion,
84
- partitionKey: partitionKey ?? "pk",
85
- sortKey: sortKey ?? "sk",
86
- sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds,
87
- dataRefresh,
88
- });
89
- this.sessionCookieOptions = cookie ?? {};
90
- return this;
91
- }
92
- setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
93
- this.sessionTokenCookieKey = sessionTokenCookieKey;
94
- this.sessionCsrfCookieKey = sessionCsrfCookieKey;
95
- return this;
79
+ if (options.cors !== undefined && options.cors !== false) {
80
+ this.corsConfig = options.cors === true ? {} : options.cors;
81
+ }
82
+ if (options.session) {
83
+ const session = options.session;
84
+ this.lambderSessionManager = new LambderSessionManager({
85
+ tableName: session.tableName,
86
+ tableRegion: session.tableRegion,
87
+ partitionKey: session.partitionKey ?? "pk",
88
+ sortKey: session.sortKey ?? "sk",
89
+ sessionSalt: session.sessionSalt,
90
+ enableSlidingExpiration: session.enableSlidingExpiration,
91
+ slidingWriteIntervalSeconds: session.slidingWriteIntervalSeconds,
92
+ dataRefresh: session.dataRefresh,
93
+ });
94
+ this.sessionCookieOptions = session.cookie ?? {};
95
+ if (session.tokenCookieKey)
96
+ this.sessionTokenCookieKey = session.tokenCookieKey;
97
+ if (session.csrfCookieKey)
98
+ this.sessionCsrfCookieKey = session.csrfCookieKey;
99
+ }
100
+ if (options.rateLimits)
101
+ this.getOrCreatePolicyEngine().setRateLimits(options.rateLimits);
102
+ if (options.guards)
103
+ this.getOrCreatePolicyEngine().addGuards(options.guards);
104
+ if (options.idempotency)
105
+ this.getOrCreatePolicyEngine().setIdempotency(options.idempotency);
96
106
  }
97
107
  setRouteFallbackHandler(routeFallbackHandler) {
98
108
  this.routeFallbackHandler = routeFallbackHandler;
@@ -162,51 +172,6 @@ export default class Lambder {
162
172
  }
163
173
  return response;
164
174
  }
165
- // ---------------------------------------------------------------------
166
- // Declarative API policies (rate limits, guards, idempotency)
167
- // ---------------------------------------------------------------------
168
- /**
169
- * Wire declarative per-API rate limiting: your LambderDdbRateLimiter
170
- * instance plus named policies, each declaring its windows and what one
171
- * counter tracks (`per`: "ip", "session", or a custom key function).
172
- * APIs then reference policies by name via the `rateLimit` option; the
173
- * returned type narrows so only declared names are accepted, and
174
- * policies keyed per "session" are only referable from addSessionApi.
175
- * Callable once; call it before the API registrations that use it.
176
- */
177
- enableApiRateLimits(config) {
178
- this.getOrCreatePolicyEngine().setRateLimits(config);
179
- return this;
180
- }
181
- /**
182
- * Wire declarative idempotency: your LambderDdbIdempotency instance plus
183
- * replay defaults. APIs opt in via `idempotency: true | { ttlSeconds }`;
184
- * the option is a type error until this is called. Requests carrying a
185
- * client `idempotencyKey` (sent by LambderCaller) claim an
186
- * identity+api+key scope atomically: concurrent duplicates refuse with
187
- * 409, replays of a completed request return the stored response, and a
188
- * crashed original releases its claim. Callable once.
189
- */
190
- enableApiIdempotency(config) {
191
- this.getOrCreatePolicyEngine().setIdempotency(config);
192
- return this;
193
- }
194
- /**
195
- * Define named guards that APIs reference (typed) via the `guards`
196
- * option. Each guard is built with lambderGuard(): its input mode
197
- * (apiInput slice of the API's own payload, a separate client-sent
198
- * guardInput, or none), an optional `session: true` requirement, an
199
- * optional parameter APIs pass in their declaration (`guards: { name:
200
- * param }`), and an optional return value that lands typed on the
201
- * handler's ctx.guardData[name]. Guards run before input validation, in
202
- * the order the API declares them; a handler refuses by throwing
203
- * (typically refuse()). Callable multiple times so domain modules can
204
- * contribute their own; names must not collide.
205
- */
206
- defineApiGuards(guards) {
207
- this.getOrCreatePolicyEngine().addGuards(guards);
208
- return this;
209
- }
210
175
  getOrCreatePolicyEngine() {
211
176
  if (!this.apiPolicyEngine)
212
177
  this.apiPolicyEngine = new LambderApiPolicyEngine();
@@ -222,7 +187,7 @@ export default class Lambder {
222
187
  if (!usesPolicies)
223
188
  return;
224
189
  if (!this.apiPolicyEngine) {
225
- throw new Error(`Lambder: API "${name}" declares rateLimit/guards/idempotency, but none of enableApiRateLimits()/defineApiGuards()/enableApiIdempotency() was called first.`);
190
+ throw new Error(`Lambder: API "${name}" declares rateLimit/guards/idempotency, but none of rateLimits/guards/idempotency was configured at creation.`);
226
191
  }
227
192
  this.apiPolicyEngine.assertRegistration(name, mode, options);
228
193
  }
@@ -345,7 +310,7 @@ export default class Lambder {
345
310
  }
346
311
  getSessionController(ctx) {
347
312
  if (!this.lambderSessionManager)
348
- throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
313
+ throw new Error("Session is not enabled. Configure the session option at creation.");
349
314
  return new LambderSessionController({
350
315
  lambderSessionManager: this.lambderSessionManager,
351
316
  sessionTokenCookieKey: this.sessionTokenCookieKey,
@@ -582,6 +547,46 @@ export default class Lambder {
582
547
  }
583
548
  }
584
549
  }
550
+ /**
551
+ * The canonical way to create an instance: fix the session data type first,
552
+ * then create with the full configuration in one declaration; the policy,
553
+ * guard, and idempotency types are INFERRED from the options, so the
554
+ * instance is born fully typed and `typeof lambderApp` is the annotation
555
+ * type for api modules. No enable/define chain exists, so there are no
556
+ * ordering rules and nothing can be half-configured.
557
+ *
558
+ * ```typescript
559
+ * // app.ts (imports no api modules, so modules can import the type back)
560
+ * export const lambderApp = initLambder<SessionData>().create({
561
+ * apiPath: "/api",
562
+ * session: { tableName: "app-session", tableRegion: "us-east-1", sessionSalt: "..." },
563
+ * rateLimits: { limiter, policies },
564
+ * guards,
565
+ * idempotency: { store },
566
+ * });
567
+ * export type AppLambder = typeof lambderApp;
568
+ *
569
+ * // orders.ts
570
+ * export const orderApi = (lambder: AppLambder) => lambder.addSessionApi(...);
571
+ *
572
+ * // index.ts: registration only
573
+ * const lambder = lambderApp.addHook(...).use(orderApi)...;
574
+ * export const handler = lambder.getHandler();
575
+ * ```
576
+ *
577
+ * Why curried (`initLambder<S>().create(...)` rather than
578
+ * `new Lambder<S>(...)`): TypeScript type arguments are all-or-nothing per
579
+ * call, so explicitly passing the session data type to the constructor
580
+ * would silently WIDEN the inferred policy and guard types to their {}
581
+ * defaults. Fixing the session type in the first call lets the second call
582
+ * infer everything else from the options. `new Lambder(options)` remains
583
+ * for untyped or session-data-free instances.
584
+ */
585
+ export const initLambder = () => ({
586
+ create(options) {
587
+ return new Lambder(options);
588
+ },
589
+ });
585
590
  /** Rebuild the query string from the API Gateway event for redirects. */
586
591
  const buildQueryString = (ctx) => {
587
592
  if (isV2HttpEvent(ctx.event)) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Lambder from './core/Lambder.js';
2
2
  export default Lambder;
3
+ export { initLambder } from './core/Lambder.js';
3
4
  export { default as LambderCaller } from "./client/LambderCaller.js";
4
5
  export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions, LambderIdempotencyKeyScope } from "./client/LambderCaller.js";
5
6
  export { LambderApiError, isLambderApiError, refuse } from "./shared/LambderApiError.js";
@@ -13,7 +14,7 @@ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtm
13
14
  export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
14
15
  export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
15
16
  export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
16
- export type { LambderRouteMatcher, LambderCorsConfig, LambderConstructorOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, LambderApp, } from "./core/Lambder.js";
17
+ export type { LambderRouteMatcher, LambderCorsConfig, LambderCreateOptions, LambderSessionOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, } from "./core/Lambder.js";
17
18
  export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
18
19
  export type { LambderPublicFilesOptions } from "./core/LambderPublicFiles.js";
19
20
  export type { LambderSessionCookieOptions } from "./session/LambderSessionController.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Lambder from './core/Lambder.js';
2
2
  export default Lambder;
3
+ export { initLambder } from './core/Lambder.js';
3
4
  export { default as LambderCaller } from "./client/LambderCaller.js";
4
5
  // Typed API refusals (isomorphic: shared code may throw them from anywhere)
5
6
  export { LambderApiError, isLambderApiError, refuse } from "./shared/LambderApiError.js";
@@ -47,7 +47,7 @@ export class LambderApiGuardsEngine {
47
47
  for (const { name } of toGuardEntries(guardsOption)) {
48
48
  const guardDef = this.guards[name];
49
49
  if (!guardDef) {
50
- throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Define it via defineApiGuards() before registering the API.`);
50
+ throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation.`);
51
51
  }
52
52
  if (guardDef.session && mode !== "session") {
53
53
  throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true), which requires addSessionApi.`);
@@ -19,7 +19,7 @@ export declare class LambderApiIdempotencyEngine {
19
19
  private defaultTtlSeconds;
20
20
  private failOpen;
21
21
  configure(config: LambderApiIdempotencyConfig): void;
22
- /** True once enableApiIdempotency() ran; registration asserts check it. */
22
+ /** True once the idempotency option was configured; registration asserts check it. */
23
23
  get isConfigured(): boolean;
24
24
  /**
25
25
  * The request's idempotencyKey: null when absent, the key when valid, a
@@ -20,12 +20,12 @@ export class LambderApiIdempotencyEngine {
20
20
  failOpen = true;
21
21
  configure(config) {
22
22
  if (this.store)
23
- throw new Error("Lambder: enableApiIdempotency() was already called.");
23
+ throw new Error("Lambder: idempotency was already configured.");
24
24
  this.store = config.store;
25
25
  this.defaultTtlSeconds = config.defaultTtlSeconds ?? 24 * 3600;
26
26
  this.failOpen = config.failOpen ?? true;
27
27
  }
28
- /** True once enableApiIdempotency() ran; registration asserts check it. */
28
+ /** True once the idempotency option was configured; registration asserts check it. */
29
29
  get isConfigured() { return this.store !== null; }
30
30
  /**
31
31
  * The request's idempotencyKey: null when absent, the key when valid, a
@@ -16,7 +16,7 @@ type LambderApiPolicyOptions = {
16
16
  * ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
17
17
  * registrations against them at startup, and executes them around handlers
18
18
  * at request time. Internal to Lambder; apps interact through
19
- * enableApiRateLimits(), enableApiIdempotency(), defineApiGuards() and the
19
+ * the create() options (rateLimits, guards, idempotency) and the
20
20
  * per-API options.
21
21
  */
22
22
  export declare class LambderApiPolicyEngine {
@@ -7,7 +7,7 @@ import { LambderApiIdempotencyEngine } from "./LambderApiIdempotency.js";
7
7
  * ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
8
8
  * registrations against them at startup, and executes them around handlers
9
9
  * at request time. Internal to Lambder; apps interact through
10
- * enableApiRateLimits(), enableApiIdempotency(), defineApiGuards() and the
10
+ * the create() options (rateLimits, guards, idempotency) and the
11
11
  * per-API options.
12
12
  */
13
13
  export class LambderApiPolicyEngine {
@@ -28,7 +28,7 @@ export class LambderApiPolicyEngine {
28
28
  this.rateLimits.assertRegistration(apiName, mode, options.rateLimit);
29
29
  this.guards.assertRegistration(apiName, mode, options.guards);
30
30
  if (options.idempotency !== undefined && !this.idempotency.isConfigured) {
31
- throw new Error(`Lambder: API "${apiName}" declares idempotency but enableApiIdempotency() was not called first.`);
31
+ throw new Error(`Lambder: API "${apiName}" declares idempotency but no idempotency store was configured at creation.`);
32
32
  }
33
33
  }
34
34
  /** Rate limits then guards, in declared order. Refusals throw (LambderApiError or a guard's own throw). */
@@ -14,7 +14,7 @@ export class LambderApiRateLimitsEngine {
14
14
  policies = {};
15
15
  configure(config) {
16
16
  if (this.limiter)
17
- throw new Error("Lambder: enableApiRateLimits() was already called.");
17
+ throw new Error("Lambder: rateLimits were already configured.");
18
18
  for (const [name, policy] of Object.entries(config.policies)) {
19
19
  const per = policy.per;
20
20
  if (!per || (per !== "ip" && per !== "session" && typeof per.handler !== "function")) {
@@ -32,7 +32,7 @@ export class LambderApiRateLimitsEngine {
32
32
  for (const name of toList(rateLimitOption)) {
33
33
  const policy = this.policies[name];
34
34
  if (!policy) {
35
- throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it via enableApiRateLimits() before registering the API.`);
35
+ throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation.`);
36
36
  }
37
37
  if (policy.per === "session" && mode !== "session") {
38
38
  throw new Error(`Lambder: API "${apiName}" uses rate-limit policy "${name}" (per "session"), which requires addSessionApi.`);
@@ -37,7 +37,7 @@ export default class LambderSessionController<TSessionData = any> {
37
37
  isSessionValid(session: any): boolean;
38
38
  updateSessionData(newData: any): Promise<LambderSessionContext>;
39
39
  /**
40
- * Force-runs the dataRefresh callback now (see enableDdbSession) and
40
+ * Force-runs the dataRefresh callback now (see the session option of create) and
41
41
  * persists the result onto the current session. Returns the updated
42
42
  * session, or null when the callback ended it: the record is deleted and
43
43
  * the session cookies are cleared.
@@ -124,7 +124,7 @@ export default class LambderSessionController {
124
124
  }
125
125
  ;
126
126
  /**
127
- * Force-runs the dataRefresh callback now (see enableDdbSession) and
127
+ * Force-runs the dataRefresh callback now (see the session option of create) and
128
128
  * persists the result onto the current session. Returns the updated
129
129
  * session, or null when the callback ended it: the record is deleted and
130
130
  * the session cookies are cleared.
@@ -230,7 +230,7 @@ export default class LambderSessionManager {
230
230
  */
231
231
  async refreshSessionData(session) {
232
232
  if (!this.dataRefresh)
233
- throw new Error("dataRefresh is not configured. Pass dataRefresh to enableDdbSession(...) to enable.");
233
+ throw new Error("dataRefresh is not configured. Pass session.dataRefresh at creation to enable.");
234
234
  if (!session)
235
235
  throw new Error("Invalid session");
236
236
  let newData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "4.0.1",
3
+ "version": "4.1.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",