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 +68 -44
- package/dist/core/Lambder.d.ts +89 -82
- package/dist/core/Lambder.js +77 -72
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/policies/LambderApiGuards.js +1 -1
- package/dist/policies/LambderApiIdempotency.d.ts +1 -1
- package/dist/policies/LambderApiIdempotency.js +2 -2
- package/dist/policies/LambderApiPolicies.d.ts +1 -1
- package/dist/policies/LambderApiPolicies.js +2 -2
- package/dist/policies/LambderApiRateLimits.js +2 -2
- package/dist/session/LambderSessionController.d.ts +1 -1
- package/dist/session/LambderSessionController.js +1 -1
- package/dist/session/LambderSessionManager.js +1 -1
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
|
75
|
+
import { initLambder } from 'lambder';
|
|
68
76
|
import { z } from 'zod';
|
|
69
77
|
import * as path from 'path';
|
|
70
78
|
|
|
71
|
-
|
|
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
|
-
|
|
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 `
|
|
287
|
+
Enable DynamoDB-based sessions with the `session` option at creation:
|
|
281
288
|
|
|
282
289
|
```typescript
|
|
283
|
-
lambder
|
|
284
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
510
|
+
import { initLambder, LambderDdbRateLimiter, LambderDdbIdempotency, lambderGuard, lambderRateLimitKey, refuse } from "lambder";
|
|
499
511
|
|
|
500
|
-
const lambder =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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.
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
86
|
-
* @typeParam _TGuards - @internal Guard
|
|
87
|
-
* @typeParam _TIdempotencyEnabled - @internal
|
|
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 =
|
|
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?:
|
|
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
|
|
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
|
|
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
|
|
297
|
-
*
|
|
298
|
-
*
|
|
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
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
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
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
idempotency
|
|
317
|
-
}
|
|
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
|
+
};
|
package/dist/core/Lambder.js
CHANGED
|
@@ -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
|
|
18
|
-
* @typeParam _TGuards - @internal Guard
|
|
19
|
-
* @typeParam _TIdempotencyEnabled - @internal
|
|
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 =
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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}".
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|