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.
- package/Readme.md +184 -41
- package/dist/Lambder.d.ts +154 -46
- package/dist/Lambder.js +312 -166
- package/dist/LambderCaller.js +6 -3
- package/dist/LambderContext.d.ts +20 -9
- package/dist/LambderContext.js +57 -17
- package/dist/LambderCors.d.ts +12 -0
- package/dist/LambderCors.js +30 -0
- package/dist/LambderDdbCache.d.ts +65 -0
- package/dist/LambderDdbCache.js +480 -0
- package/dist/LambderHtml.d.ts +33 -0
- package/dist/LambderHtml.js +62 -0
- package/dist/LambderMSW.d.ts +16 -1
- package/dist/LambderMSW.js +5 -9
- package/dist/LambderPublicFiles.d.ts +47 -0
- package/dist/LambderPublicFiles.js +108 -0
- package/dist/LambderResolver.d.ts +30 -31
- package/dist/LambderResolver.js +29 -43
- package/dist/LambderResponse.d.ts +71 -0
- package/dist/LambderResponse.js +196 -0
- package/dist/LambderResponseBuilder.d.ts +58 -33
- package/dist/LambderResponseBuilder.js +114 -167
- package/dist/LambderRouting.d.ts +23 -0
- package/dist/LambderRouting.js +67 -0
- package/dist/LambderSessionController.d.ts +13 -1
- package/dist/LambderSessionController.js +33 -10
- package/dist/LambderSessionManager.d.ts +3 -1
- package/dist/LambderSessionManager.js +15 -6
- package/dist/LambderTemplatingEngine.d.ts +87 -0
- package/dist/LambderTemplatingEngine.js +156 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +12 -1
- package/dist/node-polyfills.d.ts +4 -2
- package/dist/node-polyfills.js +28 -0
- package/package.json +8 -5
- package/.eslintrc.cjs +0 -26
- package/.vscode/settings.json +0 -26
- package/deploy +0 -22
- package/dist/LambderUtils.d.ts +0 -10
- package/dist/LambderUtils.js +0 -70
- package/docs/DYNAMODB_SETUP.md +0 -96
- package/docs/LAMBDER_MSW.md +0 -409
- package/docs/TYPE_SAFE_QUICK_START.md +0 -77
- package/examples/msw-testing-example.ts +0 -280
- package/examples/secure-session-example.ts +0 -207
- package/examples/zod-chained-api-example.ts +0 -63
- package/src/Lambder.ts +0 -430
- package/src/LambderApiContract.ts +0 -20
- package/src/LambderCaller.ts +0 -238
- package/src/LambderContext.ts +0 -78
- package/src/LambderMSW.ts +0 -180
- package/src/LambderResolver.ts +0 -101
- package/src/LambderResponseBuilder.ts +0 -332
- package/src/LambderSessionController.ts +0 -114
- package/src/LambderSessionManager.ts +0 -217
- package/src/LambderUtils.ts +0 -75
- package/src/index.ts +0 -17
- package/src/node-polyfills.ts +0 -27
- package/tests/error-handling.test.ts +0 -585
- package/tests/file-serving.test.ts +0 -194
- package/tests/fixtures/public/index.html +0 -1
- package/tests/fixtures/public/main.css +0 -1
- package/tests/hooks.test.ts +0 -561
- package/tests/output-type-runtime.test.ts +0 -381
- package/tests/redirect.test.ts +0 -88
- package/tests/routes.test.ts +0 -543
- package/tests/session.test.ts +0 -1083
- package/tests/use-plugin.test.ts +0 -460
- package/tsconfig.json +0 -24
package/dist/LambderContext.d.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
10
|
+
pathParams: TPathParams;
|
|
7
11
|
method: string;
|
|
8
|
-
get: Record<string,
|
|
12
|
+
get: Record<string, string | undefined>;
|
|
9
13
|
post: Record<string, any>;
|
|
10
|
-
cookie: Record<string,
|
|
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
|
-
|
|
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:
|
|
45
|
+
export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
|
package/dist/LambderContext.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
51
|
+
rawBody = event.isBase64Encoded
|
|
52
|
+
? (event.body ? Buffer.from(event.body, "base64").toString() : "")
|
|
53
|
+
: (event.body || "");
|
|
14
54
|
try {
|
|
15
|
-
post = JSON.parse(
|
|
55
|
+
post = JSON.parse(rawBody || "{}") || {};
|
|
16
56
|
}
|
|
17
57
|
catch (e) {
|
|
18
|
-
const params = new URLSearchParams(
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|