lambder 4.9.1 → 5.1.2

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 ADDED
@@ -0,0 +1,159 @@
1
+ # Lambder
2
+
3
+ A highly opinionated serverless web framework for TypeScript on AWS Lambda.
4
+ Lambder handles HTTP requests, routes, type-safe APIs, sessions and the
5
+ declarative policy layer around them (rate limits, authorization guards,
6
+ idempotency), so an application is a set of declarations rather than a pile of
7
+ per-handler boilerplate.
8
+
9
+ ```typescript
10
+ import { initLambder, LambderLocalFileSource } from "lambder";
11
+ import { z } from "zod";
12
+
13
+ const lambder = initLambder<SessionData>().create({
14
+ apiPath: "/api",
15
+ files: new LambderLocalFileSource({ root: "./public" }),
16
+ session: { tableName: "app-session", tableRegion: "us-east-1", sessionSalt: process.env.SESSION_SALT! },
17
+ });
18
+
19
+ lambder.addApi("getCompany", {
20
+ input: z.object({ slug: z.string() }),
21
+ output: z.object({ id: z.string(), name: z.string() }),
22
+ }, async ({ apiPayload }, res) => res.api(await loadCompany(apiPayload.slug)));
23
+
24
+ export type ApiContractType = typeof lambder.ApiContract;
25
+ export const handler = lambder.getHandler();
26
+ ```
27
+
28
+ The frontend imports that contract type and gets autocomplete, typed payloads
29
+ and typed results with no hand-written client:
30
+
31
+ ```typescript
32
+ import { LambderCaller } from "lambder/client";
33
+ import type { ApiContractType } from "./backend/handler";
34
+
35
+ const caller = new LambderCaller<ApiContractType>({ apiPath: "/api", isCorsEnabled: false });
36
+ const company = await caller.api("getCompany", { slug: "acme" });
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - **Type-safe APIs with Zod.** Define inputs and outputs with Zod schemas; get
42
+ runtime validation and compile-time inference on both sides of the wire.
43
+ - **One inferred contract.** The API contract is derived from the backend code
44
+ and consumed by the frontend as a type-only import.
45
+ - **Simple route and API declaration.** Paths, regexes, predicates and
46
+ structured matchers, chained fluently.
47
+ - **Sessions.** DynamoDB-backed, with secrets hashed at rest, sliding
48
+ expiration, data refresh and cross-subdomain cookies.
49
+ - **Declarative policies.** Named rate-limit policies, authorization guards and
50
+ idempotency, referenced by name from an API declaration and checked at
51
+ compile time.
52
+ - **A real response pipeline.** Automatic Brotli/gzip, ETag and 304 handling,
53
+ cookies, and a guard against Lambda's response size cap.
54
+ - **Hooks and actions.** Lifecycle hooks, plus `addAction()` for the non-HTTP
55
+ invocations (EventBridge, SQS, custom events) the same function receives.
56
+ - **Frontend hosting.** Serve a build from a folder, S3, R2 or any HTTP
57
+ origin, with an app shell rendered through a build-pipeline-safe template
58
+ engine.
59
+ - **Runs anywhere Lambda does.** API Gateway REST APIs (payload v1), HTTP APIs
60
+ (payload v2) and Lambda Function URLs; the payload format is detected per
61
+ event.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ npm install lambder zod
67
+ ```
68
+
69
+ `zod` and the AWS SDK clients are optional peer dependencies, so installing
70
+ lambder never drags them into your tree. Add whatever the code you actually
71
+ import needs:
72
+
73
+ | What you import | What to install alongside |
74
+ | --- | --- |
75
+ | `lambder/client` (browser, shared isomorphic code) | `zod` |
76
+ | `lambder` on AWS Lambda (`nodejs18.x` and later) | `zod`. The runtime already provides the AWS SDK v3, so mark the SDK packages as dev dependencies and keep them out of the deployment package |
77
+ | `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, `@aws-sdk/client-dynamodb`, `@aws-sdk/lib-dynamodb` |
78
+ | `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
79
+ | `lambder/testing` | `msw` |
80
+
81
+ The SDK and its `@smithy` tree are roughly 21MB installed, which is why they are
82
+ peers rather than dependencies: a frontend importing only `lambder/client` has
83
+ no use for any of it, and a Lambda deployment package should not ship a second
84
+ copy of what the runtime already loads. The runtime pins its own SDK version,
85
+ so if you need a specific one, install it and bundle it yourself.
86
+
87
+ ## Package entry points
88
+
89
+ The package ships three entry points; pick by where the code runs:
90
+
91
+ | Entry | Runs in | Carries |
92
+ | --- | --- | --- |
93
+ | `lambder` | Server (Lambda) | The full framework: pipeline, sessions, DDB stores, policies, plus everything from `lambder/client` |
94
+ | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiError`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
95
+ | `lambder/testing` | Dev and test tooling | `LambderMSW`, the MSW adapter that serves your typed contract from mock handlers |
96
+
97
+ Frontends and shared isomorphic packages should import from `lambder/client`
98
+ only; the entry's module graph contains no AWS SDK, Node built-ins, or server
99
+ pipeline, so the browser boundary is structural rather than left to
100
+ tree-shaking.
101
+
102
+ Source layout mirrors this: `src/core/` (request pipeline), `src/policies/`
103
+ (declarative rate limits, guards, idempotency), `src/session/`, `src/stores/`
104
+ (DynamoDB primitives), `src/client/`, and `src/shared/` (isomorphic modules
105
+ both entries re-export).
106
+
107
+ ## Documentation
108
+
109
+ Start with [Getting started](./docs/getting-started.md), then reach for the
110
+ guide that matches what you are building. The full index lives in
111
+ [docs/](./docs/README.md).
112
+
113
+ | Guide | Covers |
114
+ | --- | --- |
115
+ | [Getting started](./docs/getting-started.md) | The three-step path from a first API to a typed frontend call |
116
+ | [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
117
+ | [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, and non-HTTP invocations |
118
+ | [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiError` |
119
+ | [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
120
+ | [Sessions](./docs/sessions.md) | DynamoDB sessions, cookie scope, secrets at rest, `dataRefresh`, the controller API |
121
+ | [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
122
+ | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression |
123
+ | [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
124
+ | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
125
+ | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, runtime dictionaries |
126
+ | [Testing](./docs/testing.md) | `LambderMSW`: typed MSW mocking of the API contract |
127
+ | [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
128
+ | [Exports reference](./docs/exports.md) | Every name the three entry points export, grouped by purpose |
129
+
130
+ ## Standalone modules
131
+
132
+ Self-contained tools that ship with the package and work with or without the
133
+ framework:
134
+
135
+ | Module | Guide | Description |
136
+ | --- | --- | --- |
137
+ | `html` / `xml` tags + `LambderTemplatingEngine` | [Templating](./docs/templating.md) | Type-safe tagged templates and a comment-only HTML template engine (build-pipeline-safe) |
138
+ | `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension and auto language detection (isomorphic) |
139
+ | `LambderDdbCache` | [DynamoDB cache](./docs/ddb-cache.md) | DynamoDB-backed compressed JSON cache with lease-based single-fill and grouped keys (server-only) |
140
+ | `LambderDdbRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | DynamoDB fixed-window rate limiter, atomic per window (server-only) |
141
+ | `LambderDdbIdempotency` | [Idempotency store](./docs/ddb-idempotency.md) | DynamoDB idempotency records with owner-checked claims and compressed replays (server-only) |
142
+ | `LambderMSW` | [Testing](./docs/testing.md) | Typed MSW mocking of the API contract for frontend development |
143
+
144
+ ## Versioning and changes
145
+
146
+ Released versions and what each one changed are in
147
+ [CHANGELOG.md](./CHANGELOG.md). The current major is v5, which is v4's API
148
+ plus this documentation set: upgrading from 4.x needs no code changes.
149
+ Upgrading from 3.x is covered by the breaking-changes section of the 4.0.1
150
+ entry.
151
+
152
+ ## Contributing
153
+
154
+ Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to
155
+ run the tests and what a good change looks like.
156
+
157
+ ## License
158
+
159
+ MIT. See [LICENSE](./LICENSE).
@@ -9,9 +9,10 @@ export type LambderFile = {
9
9
  * servePublicFiles, serveIndexHtml, res.file and res.templateFile alike,
10
10
  * through the instance's one reader (LambderFiles). Implement `read` over
11
11
  * any backing store: LambderLocalFileSource (a folder), LambderS3FileSource
12
- * (S3, or R2 and other S3-compatible stores), or your own. The reader does
13
- * the rest for every source: traversal check, memory cache, mime fallback
14
- * from the extension.
12
+ * (S3, or R2 and other S3-compatible stores), LambderHttpFileSource (any
13
+ * origin serving files by path), or your own. The reader does the rest for
14
+ * every source: traversal check, memory cache, mime fallback from the
15
+ * extension.
15
16
  */
16
17
  export interface LambderFileSource {
17
18
  /**
@@ -48,6 +49,12 @@ export declare class LambderLocalFileSource implements LambderFileSource {
48
49
  });
49
50
  read(relativePath: string): Promise<LambderFile | null>;
50
51
  }
52
+ /**
53
+ * A file a remote store returned: the store's Content-Type unless it is a
54
+ * generic octet-stream, in which case the extension decides, as for local
55
+ * files.
56
+ */
57
+ export declare const remoteStoreFile: (body: Buffer, contentType: string | null | undefined) => LambderFile;
51
58
  /**
52
59
  * The path a source is asked for: leading slash stripped, traversal
53
60
  * rejected; null for a path that names no file (empty, or a directory).
@@ -27,6 +27,12 @@ export class LambderLocalFileSource {
27
27
  return { body: await fs.promises.readFile(absolute) };
28
28
  }
29
29
  }
30
+ /**
31
+ * A file a remote store returned: the store's Content-Type unless it is a
32
+ * generic octet-stream, in which case the extension decides, as for local
33
+ * files.
34
+ */
35
+ export const remoteStoreFile = (body, contentType) => contentType && !contentType.endsWith("octet-stream") ? { body, mimeType: contentType } : { body };
30
36
  /**
31
37
  * The path a source is asked for: leading slash stripped, traversal
32
38
  * rejected; null for a path that names no file (empty, or a directory).
package/dist/index.d.ts CHANGED
@@ -21,6 +21,8 @@ export { LambderFiles, LambderLocalFileSource } from "./core/LambderFiles.js";
21
21
  export type { LambderFileSource, LambderFile, LambderFilesOption, LambderFileMemoryCacheOption, LambderReadFile } from "./core/LambderFiles.js";
22
22
  export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
23
23
  export type { LambderS3FileSourceOptions } from "./stores/LambderS3FileSource.js";
24
+ export { LambderHttpFileSource } from "./stores/LambderHttpFileSource.js";
25
+ export type { LambderHttpFileSourceOptions } from "./stores/LambderHttpFileSource.js";
24
26
  export type { LambderSessionCookieOptions } from "./session/LambderSessionController.js";
25
27
  export type { LambderSessionContext, LambderCreatedSession, LambderSessionDataRefreshConfig } from "./session/LambderSessionManager.js";
26
28
  export { resolveCompressionOption, LAMBDER_ENCODINGS } from "./shared/LambderCompressionOption.js";
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
18
18
  export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
19
19
  export { LambderFiles, LambderLocalFileSource } from "./core/LambderFiles.js";
20
20
  export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
21
+ export { LambderHttpFileSource } from "./stores/LambderHttpFileSource.js";
21
22
  // Compression: the option every site shares, and the one codec behind them all.
22
23
  export { resolveCompressionOption, LAMBDER_ENCODINGS } from "./shared/LambderCompressionOption.js";
23
24
  // Brotli/gzip plus the bounded, length-verified restore every compressed
@@ -4,7 +4,7 @@
4
4
  * Zero dependencies, no Node/DOM requirements (browser detection is feature-gated),
5
5
  * safe to import in both lambda backends and frontend bundles.
6
6
  *
7
- * See docs/I18N.md for the full guide.
7
+ * See docs/i18n.md for the full guide.
8
8
  */
9
9
  export interface LambderLanguageMeta {
10
10
  /** Native language name (shown in language switchers). */
@@ -4,7 +4,7 @@
4
4
  * Zero dependencies, no Node/DOM requirements (browser detection is feature-gated),
5
5
  * safe to import in both lambda backends and frontend bundles.
6
6
  *
7
- * See docs/I18N.md for the full guide.
7
+ * See docs/i18n.md for the full guide.
8
8
  */
9
9
  // ---------------------------------------------------------------------------
10
10
  // Implementation
@@ -0,0 +1,31 @@
1
+ import { type LambderFile, type LambderFileSource } from "../core/LambderFiles.js";
2
+ export type LambderHttpFileSourceOptions = {
3
+ /**
4
+ * The folder URL relative paths resolve under: "https://assets.example.com/v42/".
5
+ * It names a folder, so a missing trailing slash is added.
6
+ */
7
+ baseUrl: string;
8
+ /** Sent with every read, e.g. an Authorization header for a private origin or a User-Agent a firewall allows. */
9
+ headers?: Record<string, string>;
10
+ /** How long one read may take before it fails. Default: 10000. */
11
+ timeoutMs?: number;
12
+ };
13
+ /**
14
+ * Files over HTTP(S) from any origin that serves them by path: a CDN, a
15
+ * public bucket's own domain (a Cloudflare R2 custom domain, an S3 website
16
+ * endpoint) or another server. Reads with the runtime's fetch, so it needs
17
+ * no SDK, and no credentials for a public origin; reads come out of the
18
+ * origin's edge cache. Each path segment is percent-encoded, so a relative
19
+ * path names the same object it would as an S3 key. A 404 or 410 reads as
20
+ * null and the request falls through; any other failed status, a network
21
+ * error or a timeout propagates as an error. The response's Content-Type is
22
+ * used unless it is a generic octet-stream, in which case the extension
23
+ * decides, as for local files.
24
+ */
25
+ export declare class LambderHttpFileSource implements LambderFileSource {
26
+ private readonly baseUrl;
27
+ private readonly headers;
28
+ private readonly timeoutMs;
29
+ constructor({ baseUrl, headers, timeoutMs }: LambderHttpFileSourceOptions);
30
+ read(relativePath: string): Promise<LambderFile | null>;
31
+ }
@@ -0,0 +1,46 @@
1
+ import { remoteStoreFile } from "../core/LambderFiles.js";
2
+ const DEFAULT_TIMEOUT_MS = 10_000;
3
+ /**
4
+ * Files over HTTP(S) from any origin that serves them by path: a CDN, a
5
+ * public bucket's own domain (a Cloudflare R2 custom domain, an S3 website
6
+ * endpoint) or another server. Reads with the runtime's fetch, so it needs
7
+ * no SDK, and no credentials for a public origin; reads come out of the
8
+ * origin's edge cache. Each path segment is percent-encoded, so a relative
9
+ * path names the same object it would as an S3 key. A 404 or 410 reads as
10
+ * null and the request falls through; any other failed status, a network
11
+ * error or a timeout propagates as an error. The response's Content-Type is
12
+ * used unless it is a generic octet-stream, in which case the extension
13
+ * decides, as for local files.
14
+ */
15
+ export class LambderHttpFileSource {
16
+ baseUrl;
17
+ headers;
18
+ timeoutMs;
19
+ constructor({ baseUrl, headers = {}, timeoutMs = DEFAULT_TIMEOUT_MS }) {
20
+ let url;
21
+ try {
22
+ url = new URL(baseUrl);
23
+ }
24
+ catch {
25
+ throw new Error(`baseUrl must be an absolute http(s) URL: ${baseUrl}`);
26
+ }
27
+ if (url.protocol !== "http:" && url.protocol !== "https:")
28
+ throw new Error(`baseUrl must be an absolute http(s) URL: ${baseUrl}`);
29
+ if (!url.pathname.endsWith("/"))
30
+ url.pathname += "/";
31
+ this.baseUrl = url;
32
+ this.headers = headers;
33
+ this.timeoutMs = timeoutMs;
34
+ }
35
+ async read(relativePath) {
36
+ const url = new URL(relativePath.split("/").map(encodeURIComponent).join("/"), this.baseUrl);
37
+ const response = await fetch(url, { headers: this.headers, signal: AbortSignal.timeout(this.timeoutMs) });
38
+ if (!response.ok) {
39
+ await response.body?.cancel();
40
+ if (response.status === 404 || response.status === 410)
41
+ return null;
42
+ throw new Error(`LambderHttpFileSource: ${response.status} ${response.statusText} reading ${url}`);
43
+ }
44
+ return remoteStoreFile(Buffer.from(await response.arrayBuffer()), response.headers.get("content-type"));
45
+ }
46
+ }
@@ -1,5 +1,5 @@
1
1
  import type { S3Client, S3ClientConfig } from "@aws-sdk/client-s3";
2
- import type { LambderFile, LambderFileSource } from "../core/LambderFiles.js";
2
+ import { type LambderFile, type LambderFileSource } from "../core/LambderFiles.js";
3
3
  export type LambderS3FileSourceOptions = {
4
4
  bucket: string;
5
5
  /** Literal key prefix the relative path is appended to, so include the trailing slash: "web/v42/". Default: none. */
@@ -1,3 +1,4 @@
1
+ import { remoteStoreFile } from "../core/LambderFiles.js";
1
2
  /**
2
3
  * Files from an S3 bucket, or any S3-compatible store such as Cloudflare
3
4
  * R2 (pass its endpoint in clientConfig). Needs @aws-sdk/client-s3, an
@@ -46,8 +47,6 @@ export class LambderS3FileSource {
46
47
  }
47
48
  if (!output.Body)
48
49
  return null;
49
- const body = Buffer.from(await output.Body.transformToByteArray());
50
- const contentType = output.ContentType;
51
- return contentType && !contentType.endsWith("octet-stream") ? { body, mimeType: contentType } : { body };
50
+ return remoteStoreFile(Buffer.from(await output.Body.transformToByteArray()), output.ContentType);
52
51
  }
53
52
  }
package/package.json CHANGED
@@ -1,10 +1,40 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "4.9.1",
4
- "description": "",
3
+ "version": "5.1.2",
4
+ "description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
5
+ "keywords": [
6
+ "lambda",
7
+ "aws-lambda",
8
+ "serverless",
9
+ "framework",
10
+ "typescript",
11
+ "api",
12
+ "zod",
13
+ "type-safe",
14
+ "rest",
15
+ "api-gateway",
16
+ "dynamodb",
17
+ "session",
18
+ "rate-limit",
19
+ "idempotency",
20
+ "guards",
21
+ "msw",
22
+ "i18n",
23
+ "templating"
24
+ ],
25
+ "homepage": "https://github.com/nesovera/lambder#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/nesovera/lambder/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/nesovera/lambder.git"
32
+ },
33
+ "license": "MIT",
34
+ "author": "NesoVera",
35
+ "type": "module",
5
36
  "main": "dist/index.js",
6
37
  "types": "dist/index.d.ts",
7
- "type": "module",
8
38
  "exports": {
9
39
  ".": {
10
40
  "types": "./dist/index.d.ts",
@@ -38,6 +68,9 @@
38
68
  "files": [
39
69
  "dist"
40
70
  ],
71
+ "engines": {
72
+ "node": ">=18"
73
+ },
41
74
  "scripts": {
42
75
  "typecheck": "tsc -p tsconfig.tests.json",
43
76
  "test": "npm run typecheck && vitest run",
@@ -45,12 +78,6 @@
45
78
  "build": "tsc",
46
79
  "lint": "eslint . --ext .ts,.tsx --fix"
47
80
  },
48
- "author": "",
49
- "license": "MIT",
50
- "repository": {
51
- "type": "git",
52
- "url": "https://github.com/nesovera/lambder.git"
53
- },
54
81
  "dependencies": {
55
82
  "cookie": "^1.0.2",
56
83
  "js-cookie": "^3.0.5",