lambder 3.8.1 → 4.0.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 +79 -14
- package/dist/{LambderCaller.d.ts → client/LambderCaller.d.ts} +30 -12
- package/dist/{LambderCaller.js → client/LambderCaller.js} +21 -12
- package/dist/{LambderMSW.d.ts → client/LambderMSW.d.ts} +1 -1
- package/dist/client.d.ts +16 -0
- package/dist/client.js +17 -0
- package/dist/{Lambder.d.ts → core/Lambder.d.ts} +45 -20
- package/dist/{Lambder.js → core/Lambder.js} +28 -13
- package/dist/{LambderContext.d.ts → core/LambderContext.d.ts} +9 -3
- package/dist/{LambderContext.js → core/LambderContext.js} +1 -0
- package/dist/{LambderPublicFiles.js → core/LambderPublicFiles.js} +1 -1
- package/dist/{LambderResponse.js → core/LambderResponse.js} +1 -1
- package/dist/{LambderResponseBuilder.d.ts → core/LambderResponseBuilder.d.ts} +3 -15
- package/dist/{LambderResponseBuilder.js → core/LambderResponseBuilder.js} +2 -2
- package/dist/{LambderTemplatingEngine.d.ts → core/LambderTemplatingEngine.d.ts} +1 -1
- package/dist/{LambderTemplatingEngine.js → core/LambderTemplatingEngine.js} +2 -2
- package/dist/index.d.ts +36 -35
- package/dist/index.js +21 -20
- package/dist/policies/LambderApiGuards.d.ts +221 -0
- package/dist/policies/LambderApiGuards.js +79 -0
- package/dist/policies/LambderApiIdempotency.d.ts +58 -0
- package/dist/policies/LambderApiIdempotency.js +215 -0
- package/dist/policies/LambderApiPolicies.d.ts +40 -0
- package/dist/policies/LambderApiPolicies.js +47 -0
- package/dist/policies/LambderApiRateLimits.d.ts +90 -0
- package/dist/policies/LambderApiRateLimits.js +77 -0
- package/dist/{LambderSessionController.d.ts → session/LambderSessionController.d.ts} +2 -1
- package/dist/{LambderSessionController.js → session/LambderSessionController.js} +16 -11
- package/dist/{LambderSessionManager.d.ts → session/LambderSessionManager.d.ts} +38 -4
- package/dist/{LambderSessionManager.js → session/LambderSessionManager.js} +49 -16
- package/dist/{LambderApiContract.d.ts → shared/LambderApiContract.d.ts} +14 -0
- package/dist/{LambderApiError.d.ts → shared/LambderApiError.d.ts} +1 -1
- package/dist/{LambderDdbCache.js → stores/LambderDdbCache.js} +7 -38
- package/dist/stores/LambderDdbCompression.d.ts +3 -0
- package/dist/stores/LambderDdbCompression.js +39 -0
- package/dist/{LambderDdbIdempotency.d.ts → stores/LambderDdbIdempotency.d.ts} +41 -12
- package/dist/{LambderDdbIdempotency.js → stores/LambderDdbIdempotency.js} +100 -11
- package/dist/{LambderDdbRateLimiter.js → stores/LambderDdbRateLimiter.js} +5 -1
- package/dist/testing.d.ts +9 -0
- package/dist/testing.js +8 -0
- package/package.json +19 -1
- package/dist/LambderApiPolicies.d.ts +0 -212
- package/dist/LambderApiPolicies.js +0 -228
- /package/dist/{LambderMSW.js → client/LambderMSW.js} +0 -0
- /package/dist/{LambderCors.d.ts → core/LambderCors.d.ts} +0 -0
- /package/dist/{LambderCors.js → core/LambderCors.js} +0 -0
- /package/dist/{LambderPublicFiles.d.ts → core/LambderPublicFiles.d.ts} +0 -0
- /package/dist/{LambderResolver.d.ts → core/LambderResolver.d.ts} +0 -0
- /package/dist/{LambderResolver.js → core/LambderResolver.js} +0 -0
- /package/dist/{LambderResponse.d.ts → core/LambderResponse.d.ts} +0 -0
- /package/dist/{LambderRouting.d.ts → core/LambderRouting.d.ts} +0 -0
- /package/dist/{LambderRouting.js → core/LambderRouting.js} +0 -0
- /package/dist/{LambderApiContract.js → shared/LambderApiContract.js} +0 -0
- /package/dist/{LambderApiError.js → shared/LambderApiError.js} +0 -0
- /package/dist/{LambderHtml.d.ts → shared/LambderHtml.d.ts} +0 -0
- /package/dist/{LambderHtml.js → shared/LambderHtml.js} +0 -0
- /package/dist/{LambderI18n.d.ts → shared/LambderI18n.d.ts} +0 -0
- /package/dist/{LambderI18n.js → shared/LambderI18n.js} +0 -0
- /package/dist/{node-polyfills.d.ts → shared/node-polyfills.d.ts} +0 -0
- /package/dist/{node-polyfills.js → shared/node-polyfills.js} +0 -0
- /package/dist/{LambderDdbCache.d.ts → stores/LambderDdbCache.d.ts} +0 -0
- /package/dist/{LambderDdbRateLimiter.d.ts → stores/LambderDdbRateLimiter.d.ts} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
2
|
import { LambderResponse, type HttpStatusCode, type LambderHeadersInput } from "./LambderResponse.js";
|
|
3
|
-
import { LambderSafeHtml } from "
|
|
3
|
+
import { LambderSafeHtml } from "../shared/LambderHtml.js";
|
|
4
4
|
import { type LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
5
|
+
import type { LambderApiResponseConfig } from "../shared/LambderApiContract.js";
|
|
6
|
+
export type { LambderApiResponse, LambderApiResponseConfig } from "../shared/LambderApiContract.js";
|
|
5
7
|
export type LambderResponseOptions = {
|
|
6
8
|
statusCode?: HttpStatusCode;
|
|
7
9
|
headers?: LambderHeadersInput;
|
|
@@ -12,23 +14,9 @@ export type LambderResponseOptions = {
|
|
|
12
14
|
/** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
|
|
13
15
|
etag?: boolean | "auto";
|
|
14
16
|
};
|
|
15
|
-
export type LambderApiResponseConfig = {
|
|
16
|
-
versionExpired?: boolean;
|
|
17
|
-
sessionExpired?: boolean;
|
|
18
|
-
notAuthorized?: boolean;
|
|
19
|
-
message?: any;
|
|
20
|
-
errorMessage?: any;
|
|
21
|
-
logList?: any[];
|
|
22
|
-
};
|
|
23
|
-
export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
24
|
-
apiVersion?: string | null;
|
|
25
|
-
payload?: T | null;
|
|
26
|
-
};
|
|
27
17
|
export type LambderRawResponseInit = {
|
|
28
18
|
statusCode: HttpStatusCode;
|
|
29
19
|
headers?: LambderHeadersInput;
|
|
30
|
-
/** Legacy alias for headers (API Gateway naming). */
|
|
31
|
-
multiValueHeaders?: Record<string, string[]>;
|
|
32
20
|
body: string | Buffer | null;
|
|
33
21
|
/** True when body is already a base64-encoded string. */
|
|
34
22
|
isBase64Encoded?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import mimeTypeResolver from "mime-types";
|
|
2
|
-
import { getFS, getPath } from "
|
|
2
|
+
import { getFS, getPath } from "../shared/node-polyfills.js";
|
|
3
3
|
import { LambderResponse } from "./LambderResponse.js";
|
|
4
4
|
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
5
5
|
// Compiled templates survive across requests (builder instances are per-request).
|
|
@@ -72,7 +72,7 @@ export default class LambderResponseBuilder {
|
|
|
72
72
|
raw(init) {
|
|
73
73
|
return new LambderResponse({
|
|
74
74
|
statusCode: init.statusCode,
|
|
75
|
-
headers: init.headers
|
|
75
|
+
headers: init.headers,
|
|
76
76
|
body: init.body,
|
|
77
77
|
isBodyBase64: init.isBase64Encoded ?? false,
|
|
78
78
|
compress: init.compress ?? (init.isBase64Encoded ? false : "auto"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getFS } from "
|
|
2
|
-
import { renderHtmlValue } from "
|
|
1
|
+
import { getFS } from "../shared/node-polyfills.js";
|
|
2
|
+
import { renderHtmlValue } from "../shared/LambderHtml.js";
|
|
3
3
|
const TOKEN_PATTERN = /<!--\s*(?:(slot:([\w-]+)\s*\/)|(slot:([\w-]+))|(\/slot:([\w-]+))|(if:(!?)([\w-]+))|(else)|(\/if:(!?)([\w-]+)))\s*-->/g;
|
|
4
4
|
/**
|
|
5
5
|
* Fail loudly on slot positions where HTML escaping cannot protect against
|
package/dist/index.d.ts
CHANGED
|
@@ -1,36 +1,37 @@
|
|
|
1
|
-
import Lambder from './Lambder.js';
|
|
1
|
+
import Lambder from './core/Lambder.js';
|
|
2
2
|
export default Lambder;
|
|
3
|
-
export { default as LambderCaller } from "./LambderCaller.js";
|
|
4
|
-
export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions } from "./LambderCaller.js";
|
|
5
|
-
export { LambderApiError, isLambderApiError, refuse } from "./LambderApiError.js";
|
|
6
|
-
export type { LambderApiErrorOptions, LambderRefusalMessage, LambderRefuseOptions } from "./LambderApiError.js";
|
|
7
|
-
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
8
|
-
export { default as LambderResolver } from "./LambderResolver.js";
|
|
9
|
-
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
10
|
-
export { default as LambderSessionController } from "./LambderSessionController.js";
|
|
11
|
-
export {
|
|
12
|
-
export type
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export type {
|
|
17
|
-
export
|
|
18
|
-
export type {
|
|
19
|
-
export {
|
|
20
|
-
export type {
|
|
21
|
-
export
|
|
22
|
-
export
|
|
23
|
-
export {
|
|
24
|
-
export {
|
|
25
|
-
export type {
|
|
26
|
-
export {
|
|
27
|
-
export type {
|
|
28
|
-
export {
|
|
29
|
-
export type {
|
|
30
|
-
export {
|
|
31
|
-
export type {
|
|
32
|
-
export {
|
|
33
|
-
export
|
|
34
|
-
export {
|
|
35
|
-
export type
|
|
36
|
-
export {
|
|
3
|
+
export { default as LambderCaller } from "./client/LambderCaller.js";
|
|
4
|
+
export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions, LambderIdempotencyKeyScope } from "./client/LambderCaller.js";
|
|
5
|
+
export { LambderApiError, isLambderApiError, refuse } from "./shared/LambderApiError.js";
|
|
6
|
+
export type { LambderApiErrorOptions, LambderRefusalMessage, LambderRefuseOptions } from "./shared/LambderApiError.js";
|
|
7
|
+
export { default as LambderResponseBuilder } from "./core/LambderResponseBuilder.js";
|
|
8
|
+
export { default as LambderResolver } from "./core/LambderResolver.js";
|
|
9
|
+
export { default as LambderSessionManager } from "./session/LambderSessionManager.js";
|
|
10
|
+
export { default as LambderSessionController } from "./session/LambderSessionController.js";
|
|
11
|
+
export { LambderResponse, finalizeResponse, acceptsEncoding, type HttpStatusCode, type LambderHttpResponse, type LambderHttpEventFormat, type LambderHeadersInput, type LambderFinalizeOptions, } from "./core/LambderResponse.js";
|
|
12
|
+
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
|
|
13
|
+
export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
|
|
14
|
+
export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
|
|
15
|
+
export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
|
|
16
|
+
export type { LambderRouteMatcher, LambderCorsConfig, LambderConstructorOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, LambderApp, } from "./core/Lambder.js";
|
|
17
|
+
export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
|
|
18
|
+
export type { LambderPublicFilesOptions } from "./core/LambderPublicFiles.js";
|
|
19
|
+
export type { LambderSessionCookieOptions } from "./session/LambderSessionController.js";
|
|
20
|
+
export type { LambderSessionContext, LambderCreatedSession, LambderSessionDataRefreshConfig } from "./session/LambderSessionManager.js";
|
|
21
|
+
export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
|
|
22
|
+
export { LambderDdbCache } from "./stores/LambderDdbCache.js";
|
|
23
|
+
export type { LambderDdbCacheOptions, LambderDdbCacheSetOptions, LambderDdbCacheGetOrSetOptions, } from "./stores/LambderDdbCache.js";
|
|
24
|
+
export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
|
|
25
|
+
export type { LambderDdbRateLimiterOptions, LambderRateLimitPolicy, LambderRateLimitExceededMap, LambderRateLimitResult, } from "./stores/LambderDdbRateLimiter.js";
|
|
26
|
+
export { LambderDdbIdempotency } from "./stores/LambderDdbIdempotency.js";
|
|
27
|
+
export type { LambderDdbIdempotencyOptions, LambderIdempotencyBeginResult, LambderIdempotencyDoneRecord, } from "./stores/LambderDdbIdempotency.js";
|
|
28
|
+
export { lambderGuard } from "./policies/LambderApiGuards.js";
|
|
29
|
+
export type { LambderApiGuard, LambderGuardMeta, LambderGuardMetaMap, LambderAllowedGuardNames, LambderParamlessGuardNames, LambderGuardsOption, LambderGuardsOptionValue, LambderGuardDataOf, LambderGuardInputsOf, } from "./policies/LambderApiGuards.js";
|
|
30
|
+
export { lambderRateLimitKey } from "./policies/LambderApiRateLimits.js";
|
|
31
|
+
export type { LambderRateLimitKeyFn, LambderRateLimitPer, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, } from "./policies/LambderApiRateLimits.js";
|
|
32
|
+
export type { LambderApiIdempotencyConfig } from "./policies/LambderApiIdempotency.js";
|
|
33
|
+
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
34
|
+
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
35
|
+
export { type ApiContractShape, type LambderApiResponse, type LambderApiResponseConfig, } from "./shared/LambderApiContract.js";
|
|
36
|
+
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent } from "./core/LambderContext.js";
|
|
37
|
+
export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
import Lambder from './Lambder.js';
|
|
1
|
+
import Lambder from './core/Lambder.js';
|
|
2
2
|
export default Lambder;
|
|
3
|
-
export { default as LambderCaller } from "./LambderCaller.js";
|
|
3
|
+
export { default as LambderCaller } from "./client/LambderCaller.js";
|
|
4
4
|
// Typed API refusals (isomorphic: shared code may throw them from anywhere)
|
|
5
|
-
export { LambderApiError, isLambderApiError, refuse } from "./LambderApiError.js";
|
|
6
|
-
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
7
|
-
export { default as LambderResolver } from "./LambderResolver.js";
|
|
8
|
-
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
9
|
-
export { default as LambderSessionController } from "./LambderSessionController.js";
|
|
10
|
-
export { default as LambderMSW } from "./LambderMSW.js";
|
|
5
|
+
export { LambderApiError, isLambderApiError, refuse } from "./shared/LambderApiError.js";
|
|
6
|
+
export { default as LambderResponseBuilder } from "./core/LambderResponseBuilder.js";
|
|
7
|
+
export { default as LambderResolver } from "./core/LambderResolver.js";
|
|
8
|
+
export { default as LambderSessionManager } from "./session/LambderSessionManager.js";
|
|
9
|
+
export { default as LambderSessionController } from "./session/LambderSessionController.js";
|
|
11
10
|
// Response model
|
|
12
|
-
export { LambderResponse, finalizeResponse, acceptsEncoding, } from "./LambderResponse.js";
|
|
11
|
+
export { LambderResponse, finalizeResponse, acceptsEncoding, } from "./core/LambderResponse.js";
|
|
13
12
|
// Type-safe templating (tagged templates with auto-escaping)
|
|
14
|
-
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml } from "./LambderHtml.js";
|
|
13
|
+
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml } from "./shared/LambderHtml.js";
|
|
15
14
|
// Comment-based HTML templating engine (build-pipeline-safe slots and conditionals, standalone)
|
|
16
|
-
export { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
15
|
+
export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
|
|
17
16
|
// Public file serving
|
|
18
|
-
export { LambderPublicFilesHandler } from "./LambderPublicFiles.js";
|
|
19
|
-
export { LambderSessionDataRefreshError } from "./LambderSessionManager.js";
|
|
17
|
+
export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
|
|
18
|
+
export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
|
|
20
19
|
// DynamoDB-backed compressed cache (standalone, server-only)
|
|
21
|
-
export { LambderDdbCache } from "./LambderDdbCache.js";
|
|
20
|
+
export { LambderDdbCache } from "./stores/LambderDdbCache.js";
|
|
22
21
|
// DynamoDB-backed fixed-window rate limiter (standalone, server-only)
|
|
23
|
-
export { LambderDdbRateLimiter } from "./LambderDdbRateLimiter.js";
|
|
22
|
+
export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
|
|
24
23
|
// DynamoDB-backed idempotency records (standalone, server-only)
|
|
25
|
-
export { LambderDdbIdempotency } from "./LambderDdbIdempotency.js";
|
|
26
|
-
// Declarative per-API policies
|
|
27
|
-
export { lambderGuard
|
|
24
|
+
export { LambderDdbIdempotency } from "./stores/LambderDdbIdempotency.js";
|
|
25
|
+
// Declarative per-API policies: guards
|
|
26
|
+
export { lambderGuard } from "./policies/LambderApiGuards.js";
|
|
27
|
+
// Declarative per-API policies: rate limits
|
|
28
|
+
export { lambderRateLimitKey } from "./policies/LambderApiRateLimits.js";
|
|
28
29
|
// Typed translations (standalone, isomorphic)
|
|
29
|
-
export { createLambderI18n } from "./LambderI18n.js";
|
|
30
|
-
export { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
30
|
+
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
31
|
+
export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { LambderRenderContext, LambderSessionRenderContext } from "../core/LambderContext.js";
|
|
3
|
+
import type LambderResolver from "../core/LambderResolver.js";
|
|
4
|
+
/**
|
|
5
|
+
* A named guard, run before the API's own input validation. Three input
|
|
6
|
+
* modes:
|
|
7
|
+
*
|
|
8
|
+
* - `apiInput`: the guard checks fields of the API's OWN payload. The slice
|
|
9
|
+
* is validated against the raw payload before `handler` runs and handed to
|
|
10
|
+
* it typed. The API's input schema stays the owner of those fields:
|
|
11
|
+
* declaring the guard on an API whose schema does not carry them is a
|
|
12
|
+
* compile error.
|
|
13
|
+
* - `guardInput`: the guard has its own value the client sends SEPARATELY,
|
|
14
|
+
* outside the API payload, via the caller's options.guardInputs[name].
|
|
15
|
+
* The requirement lands on the API's contract (`guardInputs`), so the
|
|
16
|
+
* typed caller refuses to compile a call that does not send it. The API
|
|
17
|
+
* payload and handler never see the value.
|
|
18
|
+
* - neither: the guard reads only the context.
|
|
19
|
+
*
|
|
20
|
+
* Orthogonally, a guard may also:
|
|
21
|
+
*
|
|
22
|
+
* - declare `session: true`: the guard needs ctx.session, so it is only
|
|
23
|
+
* declarable on addSessionApi (compile error and startup assert on public
|
|
24
|
+
* APIs) and its handler receives the session-typed context.
|
|
25
|
+
* - take a PARAMETER: annotate a 4th handler argument
|
|
26
|
+
* (`(ctx, payload, res, param: YourType) => ...`) and APIs pass the value
|
|
27
|
+
* in their declaration: `guards: { yourGuard: paramValue }`. The value is
|
|
28
|
+
* trusted registration-time code (never client data), typed per guard.
|
|
29
|
+
* - RETURN a value: whatever the handler returns (awaited) is attached to
|
|
30
|
+
* the API handler's context as `ctx.guardData[guardName]`, fully typed.
|
|
31
|
+
* Guards that return nothing never appear in guardData.
|
|
32
|
+
*
|
|
33
|
+
* A validation failure answers the standard 422 shape, and the handler
|
|
34
|
+
* refuses by throwing (typically refuse()/LambderApiError). Build with
|
|
35
|
+
* lambderGuard() so the handler's payload/ctx/param types line up.
|
|
36
|
+
*/
|
|
37
|
+
export type LambderApiGuard<TInput extends z.ZodTypeAny = z.ZodTypeAny, TParam = any, TOutput = any> = {
|
|
38
|
+
apiInput: TInput;
|
|
39
|
+
guardInput?: undefined;
|
|
40
|
+
session?: boolean;
|
|
41
|
+
handler: (ctx: any, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
42
|
+
} | {
|
|
43
|
+
guardInput: TInput;
|
|
44
|
+
apiInput?: undefined;
|
|
45
|
+
session?: boolean;
|
|
46
|
+
handler: (ctx: any, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
47
|
+
} | {
|
|
48
|
+
apiInput?: undefined;
|
|
49
|
+
guardInput?: undefined;
|
|
50
|
+
session?: boolean;
|
|
51
|
+
handler: (ctx: any, payload: undefined, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
52
|
+
};
|
|
53
|
+
type GuardCtx = LambderRenderContext;
|
|
54
|
+
type GuardSessionCtx = LambderSessionRenderContext<any, any>;
|
|
55
|
+
/**
|
|
56
|
+
* Builder that ties the handler's payload, context, param, and output types
|
|
57
|
+
* together inside one literal. Returns the exact shape so type extraction
|
|
58
|
+
* (mode, session, param, output) works downstream. The param type is
|
|
59
|
+
* inferred from the handler's 4th argument annotation; the output from its
|
|
60
|
+
* return type.
|
|
61
|
+
*/
|
|
62
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny, TParam = undefined, TOutput = void>(guard: {
|
|
63
|
+
apiInput: TInput;
|
|
64
|
+
session: true;
|
|
65
|
+
handler: (ctx: GuardSessionCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
66
|
+
}): {
|
|
67
|
+
apiInput: TInput;
|
|
68
|
+
guardInput?: undefined;
|
|
69
|
+
session: true;
|
|
70
|
+
handler: (ctx: GuardSessionCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
71
|
+
};
|
|
72
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny, TParam = undefined, TOutput = void>(guard: {
|
|
73
|
+
apiInput: TInput;
|
|
74
|
+
handler: (ctx: GuardCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
75
|
+
}): {
|
|
76
|
+
apiInput: TInput;
|
|
77
|
+
guardInput?: undefined;
|
|
78
|
+
session?: undefined;
|
|
79
|
+
handler: (ctx: GuardCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
80
|
+
};
|
|
81
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny, TParam = undefined, TOutput = void>(guard: {
|
|
82
|
+
guardInput: TInput;
|
|
83
|
+
session: true;
|
|
84
|
+
handler: (ctx: GuardSessionCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
85
|
+
}): {
|
|
86
|
+
guardInput: TInput;
|
|
87
|
+
apiInput?: undefined;
|
|
88
|
+
session: true;
|
|
89
|
+
handler: (ctx: GuardSessionCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
90
|
+
};
|
|
91
|
+
export declare function lambderGuard<TInput extends z.ZodTypeAny, TParam = undefined, TOutput = void>(guard: {
|
|
92
|
+
guardInput: TInput;
|
|
93
|
+
handler: (ctx: GuardCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
94
|
+
}): {
|
|
95
|
+
guardInput: TInput;
|
|
96
|
+
apiInput?: undefined;
|
|
97
|
+
session?: undefined;
|
|
98
|
+
handler: (ctx: GuardCtx, payload: z.output<TInput>, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
99
|
+
};
|
|
100
|
+
export declare function lambderGuard<TParam = undefined, TOutput = void>(guard: {
|
|
101
|
+
session: true;
|
|
102
|
+
handler: (ctx: GuardSessionCtx, payload: undefined, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
103
|
+
}): {
|
|
104
|
+
apiInput?: undefined;
|
|
105
|
+
guardInput?: undefined;
|
|
106
|
+
session: true;
|
|
107
|
+
handler: (ctx: GuardSessionCtx, payload: undefined, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
108
|
+
};
|
|
109
|
+
export declare function lambderGuard<TParam = undefined, TOutput = void>(guard: {
|
|
110
|
+
handler: (ctx: GuardCtx, payload: undefined, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
111
|
+
}): {
|
|
112
|
+
apiInput?: undefined;
|
|
113
|
+
guardInput?: undefined;
|
|
114
|
+
session?: undefined;
|
|
115
|
+
handler: (ctx: GuardCtx, payload: undefined, res: LambderResolver, param: TParam) => TOutput | Promise<TOutput>;
|
|
116
|
+
};
|
|
117
|
+
/** The param type a guard's handler declares as its 4th argument; undefined for paramless guards. */
|
|
118
|
+
type LambderGuardParamOf<G> = G extends {
|
|
119
|
+
handler: (...args: infer A) => any;
|
|
120
|
+
} ? (A extends [any, any, any, infer P, ...any[]] ? P : undefined) : undefined;
|
|
121
|
+
/** What a guard's handler returns (awaited); void for check-only guards. */
|
|
122
|
+
type LambderGuardOutputOf<G> = G extends {
|
|
123
|
+
handler: (...args: any[]) => infer R;
|
|
124
|
+
} ? Awaited<R> : never;
|
|
125
|
+
/** Per-guard metadata carried on the Lambder instance: input mode, session requirement, param type, output type. */
|
|
126
|
+
export type LambderGuardMeta<G> = (G extends {
|
|
127
|
+
apiInput: infer S extends z.ZodTypeAny;
|
|
128
|
+
} ? {
|
|
129
|
+
apiInput: z.output<S>;
|
|
130
|
+
} : G extends {
|
|
131
|
+
guardInput: infer S extends z.ZodTypeAny;
|
|
132
|
+
} ? {
|
|
133
|
+
guardInput: z.output<S>;
|
|
134
|
+
} : {}) & (G extends {
|
|
135
|
+
session: true;
|
|
136
|
+
} ? {
|
|
137
|
+
session: true;
|
|
138
|
+
} : {}) & {
|
|
139
|
+
param: LambderGuardParamOf<G>;
|
|
140
|
+
output: LambderGuardOutputOf<G>;
|
|
141
|
+
};
|
|
142
|
+
export type LambderGuardMetaMap<TGuards> = {
|
|
143
|
+
[K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
|
|
144
|
+
};
|
|
145
|
+
/** Guard names referenced by a guards option, whichever of its three forms is used. */
|
|
146
|
+
type NamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer N extends string)[] ? N : TOpt extends object ? keyof TOpt & string : never;
|
|
147
|
+
type LambderGuardNameIfPayloadOk<TGuards, K extends keyof TGuards, TPayload> = TGuards[K] extends {
|
|
148
|
+
apiInput: infer R;
|
|
149
|
+
} ? (TPayload extends R ? K : never) : K;
|
|
150
|
+
/**
|
|
151
|
+
* Guard names an API may declare: apiInput-mode guards only when the API's
|
|
152
|
+
* payload carries their fields, session guards only on session APIs.
|
|
153
|
+
*/
|
|
154
|
+
export type LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession extends boolean = true> = {
|
|
155
|
+
[K in keyof TGuards]: TGuards[K] extends {
|
|
156
|
+
session: true;
|
|
157
|
+
} ? (TIncludeSession extends true ? LambderGuardNameIfPayloadOk<TGuards, K, TPayload> : never) : LambderGuardNameIfPayloadOk<TGuards, K, TPayload>;
|
|
158
|
+
}[keyof TGuards] & string;
|
|
159
|
+
/** The allowed guard names whose handler takes no param (usable in the string/array forms). */
|
|
160
|
+
export type LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession extends boolean> = {
|
|
161
|
+
[K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]: TGuards[K] extends {
|
|
162
|
+
param: undefined;
|
|
163
|
+
} ? K & string : never;
|
|
164
|
+
}[LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards];
|
|
165
|
+
/**
|
|
166
|
+
* The per-API `guards` option: one paramless guard name, an ordered list of
|
|
167
|
+
* paramless names, or an object map that can carry each guard's param
|
|
168
|
+
* (`true` enables a paramless guard). Map entries run in insertion order.
|
|
169
|
+
*/
|
|
170
|
+
export type LambderGuardsOption<TGuards, TPayload, TIncludeSession extends boolean> = LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession> | readonly LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession>[] | {
|
|
171
|
+
readonly [K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]?: TGuards[K] extends {
|
|
172
|
+
param: undefined;
|
|
173
|
+
} ? true : TGuards[K] extends {
|
|
174
|
+
param: infer P;
|
|
175
|
+
} ? P : true;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* The typed ctx.guardData an API's handler sees: declared guards that return
|
|
179
|
+
* a value, keyed by name. Check-only (void) guards never appear.
|
|
180
|
+
*/
|
|
181
|
+
export type LambderGuardDataOf<TGuards, TOpt> = {
|
|
182
|
+
[K in NamesIn<TOpt> & keyof TGuards as [
|
|
183
|
+
TGuards[K] extends {
|
|
184
|
+
output: infer O;
|
|
185
|
+
} ? O : never
|
|
186
|
+
] extends [void] ? never : K & string]: TGuards[K] extends {
|
|
187
|
+
output: infer O;
|
|
188
|
+
} ? O : never;
|
|
189
|
+
};
|
|
190
|
+
type GuardInputsEntries<TGuards, TOpt> = {
|
|
191
|
+
[K in Extract<NamesIn<TOpt>, keyof TGuards> as TGuards[K] extends {
|
|
192
|
+
guardInput: any;
|
|
193
|
+
} ? K : never]: TGuards[K] extends {
|
|
194
|
+
guardInput: infer V;
|
|
195
|
+
} ? V : never;
|
|
196
|
+
};
|
|
197
|
+
/** The guardInputs map an API's contract requires clients to send; never when no declared guard uses guardInput mode. */
|
|
198
|
+
export type LambderGuardInputsOf<TGuards, TOpt> = keyof GuardInputsEntries<TGuards, TOpt> extends never ? never : GuardInputsEntries<TGuards, TOpt>;
|
|
199
|
+
/** The guards option's runtime shape: a name, ordered names, or a name-to-param map. */
|
|
200
|
+
export type LambderGuardsOptionValue = string | readonly string[] | Readonly<Record<string, unknown>>;
|
|
201
|
+
/**
|
|
202
|
+
* Validate a preflight input slice (an apiInput slice of the raw payload, or
|
|
203
|
+
* a guardInput value from the raw guardInputs map). Runs before the API's
|
|
204
|
+
* own validation; failures answer the same 422 shape as regular input
|
|
205
|
+
* validation. Shared with the rate-limit engine's apiInput-keyed policies.
|
|
206
|
+
*/
|
|
207
|
+
export declare const parsePreflightSlice: (input: z.ZodTypeAny, value: unknown, resolver: LambderResolver) => unknown;
|
|
208
|
+
/**
|
|
209
|
+
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
210
|
+
* API registrations against them at startup, and executes an API's declared
|
|
211
|
+
* guards during preflight. Composed into LambderApiPolicyEngine.
|
|
212
|
+
*/
|
|
213
|
+
export declare class LambderApiGuardsEngine {
|
|
214
|
+
private guards;
|
|
215
|
+
addGuards(guards: Record<string, LambderApiGuard<any, any, any>>): void;
|
|
216
|
+
/** Startup validation of one API registration's guards option. */
|
|
217
|
+
assertRegistration(apiName: string, mode: "public" | "session", guardsOption?: LambderGuardsOptionValue): void;
|
|
218
|
+
/** Run the API's guards in declared order. Refusals throw; outputs land on ctx.guardData. */
|
|
219
|
+
run(ctx: LambderRenderContext, resolver: LambderResolver, guardsOption?: LambderGuardsOptionValue): Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export function lambderGuard(guard) { return guard; }
|
|
2
|
+
/** Normalize the three guards-option forms into ordered { name, param } entries. */
|
|
3
|
+
const toGuardEntries = (value) => {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return [];
|
|
6
|
+
if (typeof value === "string")
|
|
7
|
+
return [{ name: value, param: undefined }];
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map((name) => ({ name: String(name), param: undefined }));
|
|
10
|
+
// Object form: insertion order, params passed verbatim (paramless guards
|
|
11
|
+
// are declared with `true` and their handlers take no param argument).
|
|
12
|
+
return Object.entries(value).map(([name, param]) => ({ name, param }));
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validate a preflight input slice (an apiInput slice of the raw payload, or
|
|
16
|
+
* a guardInput value from the raw guardInputs map). Runs before the API's
|
|
17
|
+
* own validation; failures answer the same 422 shape as regular input
|
|
18
|
+
* validation. Shared with the rate-limit engine's apiInput-keyed policies.
|
|
19
|
+
*/
|
|
20
|
+
export const parsePreflightSlice = (input, value, resolver) => {
|
|
21
|
+
const parsed = input.safeParse(value);
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
throw resolver.json({ error: "Input validation failed", zodError: parsed.error }, { statusCode: 422 });
|
|
24
|
+
}
|
|
25
|
+
return parsed.data;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
29
|
+
* API registrations against them at startup, and executes an API's declared
|
|
30
|
+
* guards during preflight. Composed into LambderApiPolicyEngine.
|
|
31
|
+
*/
|
|
32
|
+
export class LambderApiGuardsEngine {
|
|
33
|
+
guards = {};
|
|
34
|
+
addGuards(guards) {
|
|
35
|
+
for (const [name, guardDef] of Object.entries(guards)) {
|
|
36
|
+
if (this.guards[name])
|
|
37
|
+
throw new Error(`Lambder: guard "${name}" is already defined.`);
|
|
38
|
+
if (typeof guardDef?.handler !== "function")
|
|
39
|
+
throw new Error(`Lambder: guard "${name}" has no handler function.`);
|
|
40
|
+
if (guardDef.apiInput && guardDef.guardInput)
|
|
41
|
+
throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
|
|
42
|
+
this.guards[name] = guardDef;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Startup validation of one API registration's guards option. */
|
|
46
|
+
assertRegistration(apiName, mode, guardsOption) {
|
|
47
|
+
for (const { name } of toGuardEntries(guardsOption)) {
|
|
48
|
+
const guardDef = this.guards[name];
|
|
49
|
+
if (!guardDef) {
|
|
50
|
+
throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Define it via defineApiGuards() before registering the API.`);
|
|
51
|
+
}
|
|
52
|
+
if (guardDef.session && mode !== "session") {
|
|
53
|
+
throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true), which requires addSessionApi.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Run the API's guards in declared order. Refusals throw; outputs land on ctx.guardData. */
|
|
58
|
+
async run(ctx, resolver, guardsOption) {
|
|
59
|
+
for (const { name, param } of toGuardEntries(guardsOption)) {
|
|
60
|
+
const guardDef = this.guards[name];
|
|
61
|
+
if (!guardDef)
|
|
62
|
+
throw new Error(`Lambder: guard "${name}" is not configured.`);
|
|
63
|
+
const post = ctx.post;
|
|
64
|
+
let payload;
|
|
65
|
+
if (guardDef.apiInput) {
|
|
66
|
+
payload = parsePreflightSlice(guardDef.apiInput, post?.payload, resolver);
|
|
67
|
+
}
|
|
68
|
+
else if (guardDef.guardInput) {
|
|
69
|
+
payload = parsePreflightSlice(guardDef.guardInput, post?.guardInputs?.[name], resolver);
|
|
70
|
+
}
|
|
71
|
+
// A guard's return value becomes the handler's typed
|
|
72
|
+
// ctx.guardData[name]; check-only guards return undefined.
|
|
73
|
+
const output = await guardDef.handler(ctx, payload, resolver, param);
|
|
74
|
+
if (output !== undefined) {
|
|
75
|
+
ctx.guardData[name] = output;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LambderRenderContext } from "../core/LambderContext.js";
|
|
2
|
+
import type { LambderDdbIdempotency } from "../stores/LambderDdbIdempotency.js";
|
|
3
|
+
import { LambderResponse } from "../core/LambderResponse.js";
|
|
4
|
+
export type LambderApiIdempotencyConfig = {
|
|
5
|
+
/** Your idempotency store instance; may share the rate limiter's table (distinct key prefix). */
|
|
6
|
+
store: LambderDdbIdempotency;
|
|
7
|
+
/** Seconds a stored response replays for. Default: 86400 (24h). Per-API override: idempotency: { ttlSeconds }. */
|
|
8
|
+
defaultTtlSeconds?: number;
|
|
9
|
+
/** Skip idempotency (execute normally) when DynamoDB errors, instead of failing the request. Default: true. */
|
|
10
|
+
failOpen?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Runtime side of the idempotency subsystem: claims a per-operation scope
|
|
14
|
+
* around handler execution, replays stored responses, and settles claims.
|
|
15
|
+
* Composed into LambderApiPolicyEngine.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LambderApiIdempotencyEngine {
|
|
18
|
+
private store;
|
|
19
|
+
private defaultTtlSeconds;
|
|
20
|
+
private failOpen;
|
|
21
|
+
configure(config: LambderApiIdempotencyConfig): void;
|
|
22
|
+
/** True once enableApiIdempotency() ran; registration asserts check it. */
|
|
23
|
+
get isConfigured(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The request's idempotencyKey: null when absent, the key when valid, a
|
|
26
|
+
* 400 refusal when malformed. The minimum length matters for security:
|
|
27
|
+
* see IDEMPOTENCY_MIN_KEY_LENGTH.
|
|
28
|
+
*/
|
|
29
|
+
private readKey;
|
|
30
|
+
/**
|
|
31
|
+
* The record's scope. Session APIs scope per session, so even a leaked
|
|
32
|
+
* key cannot cross users. Public APIs scope by the key alone: the key is
|
|
33
|
+
* required to be long (and documented to be random), and identity proxies
|
|
34
|
+
* like the client IP are deliberately NOT part of the scope, because the
|
|
35
|
+
* retry idempotency exists for (a timeout followed by a network change)
|
|
36
|
+
* frequently arrives from a different IP.
|
|
37
|
+
*/
|
|
38
|
+
private scopeOf;
|
|
39
|
+
/**
|
|
40
|
+
* Replay fast path, run BEFORE rate limits and guards: a completed record
|
|
41
|
+
* answers with its stored response so a legitimate retry neither burns
|
|
42
|
+
* rate-limit quota nor re-runs guards (the original already passed them,
|
|
43
|
+
* and no handler executes). Misses fall through to the normal pipeline;
|
|
44
|
+
* store errors follow the failOpen setting.
|
|
45
|
+
*/
|
|
46
|
+
findReplay(apiName: string, ctx: LambderRenderContext): Promise<LambderResponse | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Idempotency wrapper around validation-passed handler execution. Without
|
|
49
|
+
* a client idempotencyKey the handler just runs; with one, the scope
|
|
50
|
+
* (identity + api + key) is claimed atomically: duplicates of an
|
|
51
|
+
* in-flight original refuse with 409, replays of a completed one return
|
|
52
|
+
* the stored response verbatim, and a crashed original releases its claim
|
|
53
|
+
* so a retry actually retries.
|
|
54
|
+
*/
|
|
55
|
+
withIdempotency(apiName: string, ctx: LambderRenderContext, config: boolean | {
|
|
56
|
+
ttlSeconds?: number;
|
|
57
|
+
}, exec: () => Promise<LambderResponse>): Promise<LambderResponse>;
|
|
58
|
+
}
|