lambder 2.0.18 → 3.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.
Files changed (69) hide show
  1. package/Readme.md +184 -41
  2. package/dist/Lambder.d.ts +154 -46
  3. package/dist/Lambder.js +312 -166
  4. package/dist/LambderCaller.js +6 -3
  5. package/dist/LambderContext.d.ts +20 -9
  6. package/dist/LambderContext.js +57 -17
  7. package/dist/LambderCors.d.ts +12 -0
  8. package/dist/LambderCors.js +30 -0
  9. package/dist/LambderDdbCache.d.ts +65 -0
  10. package/dist/LambderDdbCache.js +480 -0
  11. package/dist/LambderHtml.d.ts +33 -0
  12. package/dist/LambderHtml.js +62 -0
  13. package/dist/LambderMSW.d.ts +16 -1
  14. package/dist/LambderMSW.js +5 -9
  15. package/dist/LambderPublicFiles.d.ts +47 -0
  16. package/dist/LambderPublicFiles.js +108 -0
  17. package/dist/LambderResolver.d.ts +30 -31
  18. package/dist/LambderResolver.js +29 -43
  19. package/dist/LambderResponse.d.ts +71 -0
  20. package/dist/LambderResponse.js +196 -0
  21. package/dist/LambderResponseBuilder.d.ts +58 -33
  22. package/dist/LambderResponseBuilder.js +114 -167
  23. package/dist/LambderRouting.d.ts +23 -0
  24. package/dist/LambderRouting.js +67 -0
  25. package/dist/LambderSessionController.d.ts +13 -1
  26. package/dist/LambderSessionController.js +33 -10
  27. package/dist/LambderSessionManager.d.ts +3 -1
  28. package/dist/LambderSessionManager.js +15 -6
  29. package/dist/LambderTemplatingEngine.d.ts +87 -0
  30. package/dist/LambderTemplatingEngine.js +156 -0
  31. package/dist/index.d.ts +16 -2
  32. package/dist/index.js +12 -1
  33. package/dist/node-polyfills.d.ts +4 -2
  34. package/dist/node-polyfills.js +28 -0
  35. package/package.json +8 -5
  36. package/.eslintrc.cjs +0 -26
  37. package/.vscode/settings.json +0 -26
  38. package/deploy +0 -22
  39. package/dist/LambderUtils.d.ts +0 -10
  40. package/dist/LambderUtils.js +0 -70
  41. package/docs/DYNAMODB_SETUP.md +0 -96
  42. package/docs/LAMBDER_MSW.md +0 -409
  43. package/docs/TYPE_SAFE_QUICK_START.md +0 -77
  44. package/examples/msw-testing-example.ts +0 -280
  45. package/examples/secure-session-example.ts +0 -207
  46. package/examples/zod-chained-api-example.ts +0 -63
  47. package/src/Lambder.ts +0 -430
  48. package/src/LambderApiContract.ts +0 -20
  49. package/src/LambderCaller.ts +0 -238
  50. package/src/LambderContext.ts +0 -78
  51. package/src/LambderMSW.ts +0 -180
  52. package/src/LambderResolver.ts +0 -101
  53. package/src/LambderResponseBuilder.ts +0 -332
  54. package/src/LambderSessionController.ts +0 -114
  55. package/src/LambderSessionManager.ts +0 -217
  56. package/src/LambderUtils.ts +0 -75
  57. package/src/index.ts +0 -17
  58. package/src/node-polyfills.ts +0 -27
  59. package/tests/error-handling.test.ts +0 -585
  60. package/tests/file-serving.test.ts +0 -194
  61. package/tests/fixtures/public/index.html +0 -1
  62. package/tests/fixtures/public/main.css +0 -1
  63. package/tests/hooks.test.ts +0 -561
  64. package/tests/output-type-runtime.test.ts +0 -381
  65. package/tests/redirect.test.ts +0 -88
  66. package/tests/routes.test.ts +0 -543
  67. package/tests/session.test.ts +0 -1083
  68. package/tests/use-plugin.test.ts +0 -460
  69. package/tsconfig.json +0 -24
@@ -1,22 +1,33 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
1
+ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
2
2
  import type { LambderSessionContext } from "./LambderSessionManager.js";
3
- export type LambderRenderContext<TApiPayload = any> = {
3
+ import type { LambderHttpEventFormat } from "./LambderResponse.js";
4
+ export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
5
+ /** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
6
+ export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxyEventV2;
7
+ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>> = {
4
8
  host: string;
5
9
  path: string;
6
- pathParams: Record<string, any> | null;
10
+ pathParams: TPathParams;
7
11
  method: string;
8
- get: Record<string, any>;
12
+ get: Record<string, string | undefined>;
9
13
  post: Record<string, any>;
10
- cookie: Record<string, any>;
14
+ cookie: Record<string, string>;
11
15
  session: null;
12
- apiName: string;
16
+ apiName: string | null;
13
17
  apiPayload: TApiPayload;
14
18
  headers: APIGatewayProxyEventHeaders;
15
- event: APIGatewayProxyEvent;
19
+ /** Decoded request body, exactly as received (e.g. for webhook signature verification). */
20
+ rawBody: string;
21
+ /** Client IP: CF-Connecting-IP, then X-Forwarded-For, then the API Gateway source IP. */
22
+ ip: string;
23
+ /** Case-insensitive request header lookup. */
24
+ header: (name: string) => string | undefined;
25
+ event: LambderHttpEvent;
16
26
  lambdaContext: Context;
17
27
  _otherInternal: {
18
28
  isApiCall: boolean;
19
29
  requestVersion: string | null;
30
+ eventFormat: LambderHttpEventFormat;
20
31
  setHeaderFnAccumulator: {
21
32
  key: string;
22
33
  value: string | string[];
@@ -28,7 +39,7 @@ export type LambderRenderContext<TApiPayload = any> = {
28
39
  logToApiResponseAccumulator: any[];
29
40
  };
30
41
  };
31
- export type LambderSessionRenderContext<TApiPayload = any, SessionData = any> = Omit<LambderRenderContext<TApiPayload>, 'session'> & {
42
+ export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>> = Omit<LambderRenderContext<TApiPayload, TPathParams>, 'session'> & {
32
43
  session: LambderSessionContext<SessionData>;
33
44
  };
34
- export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
45
+ export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
@@ -1,21 +1,61 @@
1
1
  import cookieParser from "cookie";
2
+ /** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
3
+ export const isV2HttpEvent = (event) => !!event && typeof event === "object"
4
+ && event.version === "2.0"
5
+ && !!event.requestContext?.http;
2
6
  export const createContext = (event, lambdaContext, apiPath) => {
3
- const host = event.headers.Host || event.headers.host || "";
4
- const path = event.path;
5
- const pathParams = null;
6
- const get = event.queryStringParameters || {};
7
- const method = event.httpMethod;
8
- const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
9
- const headers = event.headers;
10
- // Decode body for the post
7
+ // Normalize the two API Gateway payload formats into one shape.
8
+ const eventFormat = isV2HttpEvent(event) ? "v2" : "v1";
9
+ let host;
10
+ let path;
11
+ let method;
12
+ let get;
13
+ let cookieHeader;
14
+ let sourceIp;
15
+ const headers = event.headers ?? {};
16
+ if (isV2HttpEvent(event)) {
17
+ host = headers.host || event.requestContext.domainName || "";
18
+ path = event.rawPath;
19
+ method = event.requestContext.http.method;
20
+ get = {};
21
+ for (const [key, value] of new URLSearchParams(event.rawQueryString ?? "").entries()) {
22
+ get[key] = value;
23
+ }
24
+ cookieHeader = (event.cookies ?? []).join("; ");
25
+ sourceIp = event.requestContext.http.sourceIp || "";
26
+ }
27
+ else {
28
+ host = headers.Host || headers.host || "";
29
+ path = event.path;
30
+ method = event.httpMethod;
31
+ get = event.queryStringParameters || {};
32
+ cookieHeader = headers.Cookie || headers.cookie || "";
33
+ sourceIp = event.requestContext?.identity?.sourceIp || "";
34
+ }
35
+ const cookie = cookieParser.parse(cookieHeader);
36
+ const lowercasedHeaders = {};
37
+ for (const [key, value] of Object.entries(headers)) {
38
+ if (value !== undefined)
39
+ lowercasedHeaders[key.toLowerCase()] = value;
40
+ }
41
+ const header = (name) => lowercasedHeaders[name.toLowerCase()];
42
+ const forwardedFor = lowercasedHeaders["x-forwarded-for"];
43
+ const ip = lowercasedHeaders["cf-connecting-ip"]
44
+ || (forwardedFor ? (forwardedFor.split(",")[0] ?? "").trim() : "")
45
+ || sourceIp
46
+ || "";
47
+ // Decode body: keep the raw string, then parse as JSON with urlencoded fallback.
48
+ let rawBody = "";
11
49
  let post = {};
12
50
  try {
13
- const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
51
+ rawBody = event.isBase64Encoded
52
+ ? (event.body ? Buffer.from(event.body, "base64").toString() : "")
53
+ : (event.body || "");
14
54
  try {
15
- post = JSON.parse(decodedBody) || {};
55
+ post = JSON.parse(rawBody || "{}") || {};
16
56
  }
17
57
  catch (e) {
18
- const params = new URLSearchParams(decodedBody);
58
+ const params = new URLSearchParams(rawBody);
19
59
  post = {};
20
60
  for (const [key, value] of params.entries()) {
21
61
  post[key] = value;
@@ -23,19 +63,19 @@ export const createContext = (event, lambdaContext, apiPath) => {
23
63
  }
24
64
  }
25
65
  catch (e) { }
26
- // Parse api variables
27
- const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
66
+ const isApiCall = !!(method === "POST" && apiPath && path === apiPath && post.apiName);
28
67
  const apiName = isApiCall ? post.apiName : null;
29
68
  const apiPayload = isApiCall ? post.payload : null;
30
- const requestVersion = isApiCall ? post.version : null;
69
+ const requestVersion = isApiCall ? (post.version ?? null) : null;
31
70
  return {
32
- host, path, pathParams, method,
71
+ host, path, pathParams: {}, method,
33
72
  get, post, cookie, event,
34
73
  session: null,
35
74
  apiName, apiPayload,
36
- headers, lambdaContext,
75
+ headers, rawBody, ip, header,
76
+ lambdaContext,
37
77
  _otherInternal: {
38
- isApiCall, requestVersion,
78
+ isApiCall, requestVersion, eventFormat,
39
79
  setHeaderFnAccumulator: [],
40
80
  addHeaderFnAccumulator: [],
41
81
  logToApiResponseAccumulator: [],
@@ -0,0 +1,12 @@
1
+ import type { LambderRenderContext } from "./LambderContext.js";
2
+ import type { LambderResponse } from "./LambderResponse.js";
3
+ export type LambderCorsConfig = {
4
+ /** "*" (default), an allowlist, or a per-request predicate. With credentials, the origin is echoed (never "*"). */
5
+ origins?: "*" | string[] | ((origin: string, ctx: LambderRenderContext) => boolean);
6
+ credentials?: boolean;
7
+ methods?: string[];
8
+ allowHeaders?: string[];
9
+ maxAge?: number;
10
+ };
11
+ /** Mutate the response with the CORS headers the config allows for this request. */
12
+ export declare const applyCorsHeaders: (config: LambderCorsConfig | null, ctx: LambderRenderContext, response: LambderResponse, isPreflight: boolean) => void;
@@ -0,0 +1,30 @@
1
+ /** Mutate the response with the CORS headers the config allows for this request. */
2
+ export const applyCorsHeaders = (config, ctx, response, isPreflight) => {
3
+ if (!config)
4
+ return;
5
+ const origin = ctx.header("origin") ?? "";
6
+ const origins = config.origins ?? "*";
7
+ let allowOrigin = null;
8
+ if (origins === "*") {
9
+ allowOrigin = config.credentials ? (origin || null) : "*";
10
+ }
11
+ else if (Array.isArray(origins)) {
12
+ allowOrigin = origin && origins.includes(origin) ? origin : null;
13
+ }
14
+ else {
15
+ allowOrigin = origin && origins(origin, ctx) ? origin : null;
16
+ }
17
+ if (!allowOrigin)
18
+ return;
19
+ response.setHeader("Access-Control-Allow-Origin", allowOrigin);
20
+ if (allowOrigin !== "*")
21
+ response.addHeader("Vary", "Origin");
22
+ if (config.credentials)
23
+ response.setHeader("Access-Control-Allow-Credentials", "true");
24
+ if (isPreflight) {
25
+ response.setHeader("Access-Control-Allow-Methods", (config.methods ?? ["GET", "POST", "OPTIONS"]).join(","));
26
+ response.setHeader("Access-Control-Allow-Headers", (config.allowHeaders ?? ["Origin", "X-Requested-With", "Content-Type", "Accept"]).join(", "));
27
+ if (config.maxAge !== undefined)
28
+ response.setHeader("Access-Control-Max-Age", String(config.maxAge));
29
+ }
30
+ };
@@ -0,0 +1,65 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ export interface LambderDdbCacheOptions {
3
+ tableName: string;
4
+ region?: string;
5
+ namespace?: string;
6
+ defaultTtlSeconds?: number;
7
+ chunkBytes?: number;
8
+ compressionQuality?: number;
9
+ maxValueBytes?: number;
10
+ memoryMaxBytes?: number;
11
+ client?: DynamoDBClient;
12
+ }
13
+ export interface LambderDdbCacheSetOptions {
14
+ ttlSeconds?: number;
15
+ }
16
+ export interface LambderDdbCacheGetOrSetOptions extends LambderDdbCacheSetOptions {
17
+ leaseSeconds?: number;
18
+ waitForFillMs?: number;
19
+ }
20
+ /**
21
+ * Persistent JSON cache backed by DynamoDB.
22
+ *
23
+ * Values are Brotli-compressed. Values within the safe DynamoDB item budget are
24
+ * stored directly in the manifest for a single-request read; larger values are
25
+ * split into versioned binary chunks. A manifest is written only after every
26
+ * chunk succeeds, so readers see either the previous complete version or the
27
+ * new complete version. DynamoDB TTL is cleanup only; every read also checks
28
+ * expiresAt because TTL deletion can lag.
29
+ */
30
+ export declare class LambderDdbCache {
31
+ readonly tableName: string;
32
+ readonly namespace: string;
33
+ private readonly client;
34
+ private readonly defaultTtlSeconds;
35
+ private readonly chunkBytes;
36
+ private readonly compressionQuality;
37
+ private readonly maxValueBytes;
38
+ private readonly memory;
39
+ private readonly inFlight;
40
+ constructor(options: LambderDdbCacheOptions);
41
+ get<T>(key: string): Promise<T | undefined>;
42
+ has(key: string): Promise<boolean>;
43
+ set<T>(key: string, value: T, options?: LambderDdbCacheSetOptions): Promise<void>;
44
+ delete(key: string): Promise<boolean>;
45
+ getOrSet<T>(key: string, factory: () => Promise<T>, options?: LambderDdbCacheGetOrSetOptions): Promise<T>;
46
+ /**
47
+ * Cache infrastructure is best-effort for getOrSet: read, lease, or write
48
+ * failures return the loader value. Loader failures still propagate and the
49
+ * loader is never repeated after it has completed successfully.
50
+ */
51
+ private getOrSetFailOpen;
52
+ private fill;
53
+ private acquireLease;
54
+ private releaseLease;
55
+ private readManifest;
56
+ private readChunks;
57
+ private invalidateManifest;
58
+ private batchWrite;
59
+ private remember;
60
+ private normalizeKey;
61
+ private partitionKey;
62
+ private chunkSortKey;
63
+ private nowSeconds;
64
+ private isConditionalFailure;
65
+ }