lambder 7.2.5 → 8.0.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/CHANGELOG.md +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -2,12 +2,14 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, APIGatewayProxyEvent
|
|
|
2
2
|
import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
|
|
3
3
|
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
4
4
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
5
|
+
import type LambderSessionController from "../session/LambderSessionController.js";
|
|
6
|
+
import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult } from "../api/LambderApiRateLimits.js";
|
|
5
7
|
export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
6
8
|
/**
|
|
7
9
|
* Which API Gateway payload format an event arrived in, and the format its
|
|
8
|
-
* response leaves in. Declared here, beside the detection it comes from
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* response leaves in. Declared here, beside the detection it comes from,
|
|
11
|
+
* rather than beside LambderResponse's emitters that read it: there, the two
|
|
12
|
+
* core modules would import each other.
|
|
11
13
|
*/
|
|
12
14
|
export type LambderHttpEventFormat = "v1" | "v2";
|
|
13
15
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
@@ -16,11 +18,32 @@ export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxy
|
|
|
16
18
|
* Everything a route or API handler knows about the request. Extends the
|
|
17
19
|
* API core's call context (session, guardData, responseHeaders, logList),
|
|
18
20
|
* which is the part the pipeline and the session controller work on; the
|
|
19
|
-
* rest is the HTTP request as the Lambda event delivered it
|
|
21
|
+
* rest is the HTTP request as the Lambda event delivered it, plus the tools
|
|
22
|
+
* the instance rendering it binds on (sessionController, rateLimit, isRateLimited).
|
|
23
|
+
*
|
|
24
|
+
* TRateLimitPolicies is the app's policies map on a handler registered with
|
|
25
|
+
* addApi, addSessionApi, addRoute or addSessionRoute, so a policy name is
|
|
26
|
+
* checked where it is charged; anywhere else (a hook, a guard) the names are
|
|
27
|
+
* any string.
|
|
20
28
|
*/
|
|
21
|
-
export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TSessionData = any
|
|
29
|
+
export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TSessionData = any, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = {
|
|
30
|
+
/**
|
|
31
|
+
* The Host the gateway received, or the first header named in
|
|
32
|
+
* `trustedHostHeaders` that carries a well-formed host. On a direct
|
|
33
|
+
* invoke, the invoking caller's `host`, whatever headers it forwarded.
|
|
34
|
+
*/
|
|
22
35
|
host: string;
|
|
36
|
+
/**
|
|
37
|
+
* The request path, decoded exactly once whichever gateway delivered it
|
|
38
|
+
* (a REST API and a Function URL deliver it encoded, an HTTP API
|
|
39
|
+
* decoded, in either payload format), with two escapes kept: a slash inside a segment stays `%2F`,
|
|
40
|
+
* so it cannot become a separator, and a percent sign stays `%25`, so no
|
|
41
|
+
* decoded text passes for an escape. What routes match and files are
|
|
42
|
+
* looked up by (see LambderRequestPath).
|
|
43
|
+
*/
|
|
23
44
|
path: string;
|
|
45
|
+
/** The path as the gateway delivered it, stage stripped: encoded or not, depending on the gateway. */
|
|
46
|
+
rawPath: string;
|
|
24
47
|
pathParams: TPathParams;
|
|
25
48
|
method: string;
|
|
26
49
|
get: Record<string, string | undefined>;
|
|
@@ -36,13 +59,12 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
36
59
|
*/
|
|
37
60
|
cookieList: Record<string, string[]>;
|
|
38
61
|
/**
|
|
39
|
-
* The session, once something read or created one: the pipeline sets it
|
|
40
|
-
* a session API, and
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* the same object it already held.
|
|
62
|
+
* The session, once something read or created one: the pipeline sets it
|
|
63
|
+
* on a session API, and ctx.sessionController.createSession writes it here too.
|
|
64
|
+
* Null everywhere else, which is why a route or public API reads it as
|
|
65
|
+
* `ctx.session?.data`. Not typed as the literal `null`, since
|
|
66
|
+
* createSession fills it and addSessionRoute hands the handler this same
|
|
67
|
+
* object.
|
|
46
68
|
*/
|
|
47
69
|
session: LambderSessionRecord<TSessionData> | null;
|
|
48
70
|
/**
|
|
@@ -68,7 +90,8 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
68
90
|
/**
|
|
69
91
|
* The address the gateway observed, or the leftmost entry of the first
|
|
70
92
|
* header named in `trustedClientIpHeaders` that carries one; nothing is
|
|
71
|
-
* trusted by default
|
|
93
|
+
* trusted by default, and no header on a direct invoke, whose address is
|
|
94
|
+
* the invoking caller's `clientIp`. One spelling per address (port and brackets
|
|
72
95
|
* stripped, lowercased, length-bounded), so a `per: "ip"` limit keys one
|
|
73
96
|
* counter per client.
|
|
74
97
|
*/
|
|
@@ -83,9 +106,53 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
83
106
|
responseHeaders: LambderAnswerHeaders;
|
|
84
107
|
/** Entries for the API envelope's logList channel (res.logToApiResponse). */
|
|
85
108
|
logList: unknown[];
|
|
109
|
+
/**
|
|
110
|
+
* Sessions for this request: read the one it carries
|
|
111
|
+
* (fetchSessionIfExists), create, rotate, refresh and end them. An API
|
|
112
|
+
* call presents its posted CSRF token and a route its cookies alone, the
|
|
113
|
+
* same controller `lambder.getSessionController(ctx)` hands out. Throws
|
|
114
|
+
* when the instance was created without the session option.
|
|
115
|
+
*/
|
|
116
|
+
sessionController: LambderSessionController<TSessionData>;
|
|
117
|
+
/**
|
|
118
|
+
* Counts one attempt against a named rate-limit policy and refuses the
|
|
119
|
+
* request when it is over: a 429 envelope on an API call, a plain 429 on
|
|
120
|
+
* a route, with Retry-After and the policy's errorMessage either way. A
|
|
121
|
+
* policy without `per` takes the key as the second argument; a `per:
|
|
122
|
+
* "ip"` or `per: "session"` one reads it off the request. The instance's
|
|
123
|
+
* limiter, failOpen and key bounding apply, as for a declared limit.
|
|
124
|
+
*/
|
|
125
|
+
rateLimit: LambderContextRateLimit<TRateLimitPolicies>;
|
|
126
|
+
/** The same count as rateLimit, answered instead of thrown: false, or the window that refused and its retryAfterSeconds. */
|
|
127
|
+
isRateLimited: LambderContextRateLimitCheck<TRateLimitPolicies>;
|
|
86
128
|
};
|
|
87
|
-
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}
|
|
129
|
+
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData, SessionData, TRateLimitPolicies>, 'session'> & {
|
|
88
130
|
session: LambderSessionRecord<SessionData>;
|
|
89
131
|
};
|
|
132
|
+
/** The members of a render context that belong to the instance rendering the request rather than to its event. */
|
|
133
|
+
type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited";
|
|
134
|
+
/** What an instance binds onto each context it renders: see bindContextTools. */
|
|
135
|
+
export type LambderContextTools = {
|
|
136
|
+
sessionControllerFor: (ctx: LambderRenderContext) => LambderSessionController<any>;
|
|
137
|
+
/** Charges a policy for ctx; `refuse` throws the refusal when it is over instead of answering the check result. */
|
|
138
|
+
chargeRateLimit: (ctx: LambderRenderContext, policy: string, key: string | undefined, refuse: boolean) => Promise<LambderRateLimitCheckResult>;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Puts one instance's tools onto a context, bound to that very object, so
|
|
142
|
+
* what they read (the session, the ip, which API the call is) is the
|
|
143
|
+
* context's own. Bound through bindCallTools, as the mock's are, and bound
|
|
144
|
+
* again by the instance on whatever context a beforeRender hook hands back.
|
|
145
|
+
*/
|
|
146
|
+
export declare const bindContextTools: (ctx: Omit<LambderRenderContext, LambderContextToolName> | LambderRenderContext, tools: LambderContextTools) => LambderRenderContext;
|
|
147
|
+
/** What createContext reads a request with: the instance's own settings for where an API call goes and which forwarded headers it trusts. */
|
|
148
|
+
export type LambderContextOptions = {
|
|
149
|
+
/** See `apiPath` at create(). Default: "/api", as there. */
|
|
150
|
+
apiPath?: string;
|
|
151
|
+
/** See `trustedClientIpHeaders` at create(). Default: none. */
|
|
152
|
+
trustedClientIpHeaders?: readonly string[];
|
|
153
|
+
/** See `trustedHostHeaders` at create(). Default: none. */
|
|
154
|
+
trustedHostHeaders?: readonly string[];
|
|
155
|
+
};
|
|
90
156
|
/** The render context for one request: everything a route handler, an API handler, a hook or a guard reads about it, built once from the Lambda event. */
|
|
91
|
-
export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, apiPath
|
|
157
|
+
export declare const createContext: (event: LambderHttpEvent, lambdaContext: Context, { apiPath, trustedClientIpHeaders, trustedHostHeaders }?: LambderContextOptions) => LambderRenderContext;
|
|
158
|
+
export {};
|
|
@@ -1,17 +1,76 @@
|
|
|
1
|
-
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
1
|
+
import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
2
2
|
import { resolveClientIp } from "../shared/util/LambderClientIp.js";
|
|
3
3
|
import { base64ToText } from "../shared/util/LambderBase64.js";
|
|
4
4
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
5
|
+
import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
|
|
6
|
+
import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
|
|
7
|
+
import { bindCallTools } from "../api/LambderApiCallContext.js";
|
|
8
|
+
import { decodeRequestPath } from "./LambderRequestPath.js";
|
|
5
9
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
6
10
|
export const isV2HttpEvent = (event) => !!event && typeof event === "object"
|
|
7
11
|
&& event.version === "2.0"
|
|
8
12
|
&& !!event.requestContext?.http;
|
|
13
|
+
/**
|
|
14
|
+
* Puts one instance's tools onto a context, bound to that very object, so
|
|
15
|
+
* what they read (the session, the ip, which API the call is) is the
|
|
16
|
+
* context's own. Bound through bindCallTools, as the mock's are, and bound
|
|
17
|
+
* again by the instance on whatever context a beforeRender hook hands back.
|
|
18
|
+
*/
|
|
19
|
+
export const bindContextTools = (ctx, tools) => {
|
|
20
|
+
const bound = ctx;
|
|
21
|
+
bindCallTools(bound, {
|
|
22
|
+
getters: { sessionController: () => tools.sessionControllerFor(bound) },
|
|
23
|
+
methods: {
|
|
24
|
+
rateLimit: async (policy, key) => { await tools.chargeRateLimit(bound, policy, key, true); },
|
|
25
|
+
isRateLimited: (policy, key) => tools.chargeRateLimit(bound, policy, key, false),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return bound;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The tools of a context no instance renders: one createContext() built from
|
|
32
|
+
* an event on its own. Touching one says what is missing instead of failing
|
|
33
|
+
* on an undefined member.
|
|
34
|
+
*/
|
|
35
|
+
const unboundTool = (member) => () => {
|
|
36
|
+
throw new Error(`Lambder: ctx.${member} is bound by the Lambder instance rendering the request, and this context was built by createContext() alone. Use lambder.getSessionController(ctx) for its session controller.`);
|
|
37
|
+
};
|
|
38
|
+
const UNBOUND_CONTEXT_TOOLS = {
|
|
39
|
+
sessionControllerFor: unboundTool("sessionController"),
|
|
40
|
+
chargeRateLimit: unboundTool("rateLimit"),
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A Function URL's own domain, `<url-id>.lambda-url.<region>.on.aws`, which
|
|
44
|
+
* its events always carry: the URL answers no other Host, so CloudFront in
|
|
45
|
+
* front of one sends it this one too. Every other gateway's v2 event is an
|
|
46
|
+
* HTTP API's, custom domains included.
|
|
47
|
+
*
|
|
48
|
+
* Asked of a gateway's event only. An event Lambder synthesized (an invoke,
|
|
49
|
+
* lambder/testing, the handler transport) carries one of Lambder's own
|
|
50
|
+
* apiIds and its path decoded, and names whatever host its caller chose,
|
|
51
|
+
* this domain included.
|
|
52
|
+
*/
|
|
53
|
+
const FUNCTION_URL_DOMAIN = /\.lambda-url\.[a-z0-9-]+\.on\.aws$/i;
|
|
54
|
+
/** A value a Host header may carry: a name or an address, and a port. Anything else from a forwarded header is not taken as the host. */
|
|
55
|
+
const HOST_VALUE_PATTERN = /^(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::\d{1,5})?$/;
|
|
56
|
+
/**
|
|
57
|
+
* The path without a named stage's prefix. An HTTP API keeps the stage in
|
|
58
|
+
* the path it delivers, in either payload format (`/prod/orders` on stage
|
|
59
|
+
* `prod`), where a REST API strips it first; `$default` is never in it.
|
|
60
|
+
*/
|
|
61
|
+
const withoutStagePrefix = (path, stage) => stage && stage !== "$default" && (path === `/${stage}` || path.startsWith(`/${stage}/`))
|
|
62
|
+
? path.slice(stage.length + 1) || "/"
|
|
63
|
+
: path;
|
|
9
64
|
/** The render context for one request: everything a route handler, an API handler, a hook or a guard reads about it, built once from the Lambda event. */
|
|
10
|
-
export const createContext = (event, lambdaContext, apiPath, trustedClientIpHeaders = []) => {
|
|
65
|
+
export const createContext = (event, lambdaContext, { apiPath = DEFAULT_API_PATH, trustedClientIpHeaders = [], trustedHostHeaders = [] } = {}) => {
|
|
11
66
|
// Normalize the two API Gateway payload formats into one shape.
|
|
12
67
|
const eventFormat = isV2HttpEvent(event) ? "v2" : "v1";
|
|
13
68
|
let host;
|
|
14
|
-
let
|
|
69
|
+
let rawPath;
|
|
70
|
+
// An HTTP API delivers the path decoded, in either payload format, and so
|
|
71
|
+
// does Lambder's own 2.0 event builder; a REST API and a Function URL
|
|
72
|
+
// deliver it as the viewer sent it.
|
|
73
|
+
let pathAlreadyDecoded = false;
|
|
15
74
|
let method;
|
|
16
75
|
let get;
|
|
17
76
|
let cookiePairs;
|
|
@@ -19,12 +78,10 @@ export const createContext = (event, lambdaContext, apiPath, trustedClientIpHead
|
|
|
19
78
|
const headers = event.headers ?? {};
|
|
20
79
|
if (isV2HttpEvent(event)) {
|
|
21
80
|
host = headers.host || event.requestContext.domainName || "";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
path = path.slice(stage.length + 1) || "/";
|
|
27
|
-
}
|
|
81
|
+
const apiId = event.requestContext.apiId;
|
|
82
|
+
pathAlreadyDecoded = apiId === LAMBDER_INVOKE_API_ID || apiId === LAMBDER_LOCAL_API_ID
|
|
83
|
+
|| !FUNCTION_URL_DOMAIN.test(event.requestContext.domainName ?? "");
|
|
84
|
+
rawPath = withoutStagePrefix(event.rawPath, event.requestContext.stage);
|
|
28
85
|
method = event.requestContext.http.method;
|
|
29
86
|
get = {};
|
|
30
87
|
for (const [key, value] of new URLSearchParams(event.rawQueryString ?? "").entries()) {
|
|
@@ -36,26 +93,54 @@ export const createContext = (event, lambdaContext, apiPath, trustedClientIpHead
|
|
|
36
93
|
}
|
|
37
94
|
else {
|
|
38
95
|
host = headers.Host || headers.host || "";
|
|
39
|
-
|
|
96
|
+
rawPath = event.path;
|
|
97
|
+
// An HTTP API sending payload format 1.0 marks it `version: "1.0"`;
|
|
98
|
+
// a REST API's event carries no version. Read as a REST API's, the
|
|
99
|
+
// decoded path would be decoded a second time: `/%2561dmin`, handed
|
|
100
|
+
// over as `/%61dmin`, would reach `/admin` past whatever authorizer
|
|
101
|
+
// guarded that route at the gateway.
|
|
102
|
+
if (event.version === "1.0") {
|
|
103
|
+
pathAlreadyDecoded = true;
|
|
104
|
+
rawPath = withoutStagePrefix(event.path, event.requestContext?.stage);
|
|
105
|
+
}
|
|
40
106
|
method = event.httpMethod;
|
|
41
107
|
get = event.queryStringParameters || {};
|
|
42
108
|
// A REST API keeps only the LAST value of a repeated header in
|
|
43
109
|
// `headers` and every value in `multiValueHeaders`, and HTTP/2 lets a
|
|
44
110
|
// client split its cookies across several Cookie headers. The session
|
|
45
|
-
// layer weighs every copy of a cookie name, so dropping one
|
|
46
|
-
//
|
|
47
|
-
// them all.
|
|
111
|
+
// layer weighs every copy of a cookie name, so dropping one drops a
|
|
112
|
+
// candidate session (v2's `event.cookies` carries them all).
|
|
48
113
|
const cookieHeaders = event.multiValueHeaders?.Cookie ?? event.multiValueHeaders?.cookie;
|
|
49
114
|
cookiePairs = (cookieHeaders?.length ? cookieHeaders.join("; ") : (headers.Cookie || headers.cookie || "")).split(";");
|
|
50
115
|
sourceIp = event.requestContext?.identity?.sourceIp || "";
|
|
51
116
|
}
|
|
117
|
+
const path = decodeRequestPath(rawPath, pathAlreadyDecoded);
|
|
52
118
|
const cookieList = cookieValuesByName(cookiePairs);
|
|
53
119
|
const cookie = Object.create(null);
|
|
54
120
|
for (const [name, values] of Object.entries(cookieList))
|
|
55
121
|
cookie[name] = values[0];
|
|
56
122
|
const lowercasedHeaders = lowercaseHeaderNames(headers);
|
|
57
123
|
const header = (name) => lowercasedHeaders[name.toLowerCase()];
|
|
58
|
-
|
|
124
|
+
// A trusted forwarding header is trusted because a proxy in front of this
|
|
125
|
+
// function writes it. A direct invoke has no such proxy: its headers are
|
|
126
|
+
// whatever the invoking code passed on, and a gateway lambda forwarding a
|
|
127
|
+
// browser's request passes on the browser's own. So on an invoke the
|
|
128
|
+
// address and the host are the ones the invoker named (clientIp and host,
|
|
129
|
+
// delivered as sourceIp and Host), and no header is read for either.
|
|
130
|
+
const invokedDirectly = event.requestContext?.apiId === LAMBDER_INVOKE_API_ID;
|
|
131
|
+
// The first trusted header carrying a well-formed host, leftmost entry,
|
|
132
|
+
// else the host the gateway saw. Behind CloudFront a Function URL sees
|
|
133
|
+
// its own lambda-url domain, since CloudFront sends an origin its own
|
|
134
|
+
// Host, so cookie domains and host routing need the viewer's host from a
|
|
135
|
+
// header the distribution writes.
|
|
136
|
+
for (const name of invokedDirectly ? [] : trustedHostHeaders) {
|
|
137
|
+
const forwarded = (lowercasedHeaders[name.toLowerCase()] ?? "").split(",")[0].trim();
|
|
138
|
+
if (HOST_VALUE_PATTERN.test(forwarded)) {
|
|
139
|
+
host = forwarded;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const ip = resolveClientIp(lowercasedHeaders, sourceIp, invokedDirectly ? [] : trustedClientIpHeaders);
|
|
59
144
|
// Decode body: keep the raw string, then parse as JSON with urlencoded fallback.
|
|
60
145
|
const rawBody = event.isBase64Encoded
|
|
61
146
|
? (event.body ? base64ToText(event.body) : "")
|
|
@@ -71,13 +156,15 @@ export const createContext = (event, lambdaContext, apiPath, trustedClientIpHead
|
|
|
71
156
|
post[key] = value;
|
|
72
157
|
}
|
|
73
158
|
}
|
|
74
|
-
// A POST to the API path whose body names an API is an API call;
|
|
75
|
-
// core reads the envelope, and everything downstream reads ctx.api.
|
|
76
|
-
|
|
159
|
+
// A JSON POST to the API path whose body names an API is an API call;
|
|
160
|
+
// the core reads the envelope, and everything downstream reads ctx.api.
|
|
161
|
+
// JSON only (isApiCallContentType): any site can submit a plain HTML form
|
|
162
|
+
// to this path, and enctype="text/plain" lays out a JSON body exactly.
|
|
163
|
+
const api = method === "POST" && !!apiPath && path === apiPath && isApiCallContentType(lowercasedHeaders)
|
|
77
164
|
? readApiEnvelope(post, { headers: lowercasedHeaders, cookies: cookieList, ip, host })
|
|
78
165
|
: null;
|
|
79
|
-
return {
|
|
80
|
-
host, path, pathParams: {}, method,
|
|
166
|
+
return bindContextTools({
|
|
167
|
+
host, path, rawPath, pathParams: {}, method,
|
|
81
168
|
get, post, cookie, cookieList, event,
|
|
82
169
|
session: null,
|
|
83
170
|
api,
|
|
@@ -89,5 +176,5 @@ export const createContext = (event, lambdaContext, apiPath, trustedClientIpHead
|
|
|
89
176
|
eventFormat,
|
|
90
177
|
responseHeaders: new LambderAnswerHeaders(),
|
|
91
178
|
logList: [],
|
|
92
|
-
};
|
|
179
|
+
}, UNBOUND_CONTEXT_TOOLS);
|
|
93
180
|
};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
2
|
import type { LambderResponse } from "./LambderResponse.js";
|
|
3
3
|
export type LambderCorsConfig = {
|
|
4
|
-
/** "*" (default), an allowlist, or a per-request predicate.
|
|
4
|
+
/** "*" (default), an allowlist, or a per-request predicate, asked once per request; one that throws counts as refused. An allowed origin is echoed back. */
|
|
5
5
|
origins?: "*" | string[] | ((origin: string, ctx: LambderRenderContext) => boolean);
|
|
6
|
+
/**
|
|
7
|
+
* Let an allowed origin make credentialed calls (cookies). Needs an
|
|
8
|
+
* allowlist or a predicate in `origins`: credentials for any origin would
|
|
9
|
+
* let every website read a signed-in user's answers, so create() refuses
|
|
10
|
+
* the pair.
|
|
11
|
+
*/
|
|
6
12
|
credentials?: boolean;
|
|
7
13
|
methods?: string[];
|
|
8
14
|
allowHeaders?: string[];
|
|
@@ -14,5 +20,17 @@ export type LambderCorsConfig = {
|
|
|
14
20
|
exposeHeaders?: string[];
|
|
15
21
|
maxAge?: number;
|
|
16
22
|
};
|
|
17
|
-
/**
|
|
18
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The Access-Control-Allow-Origin this request earns: "*", the echoed origin,
|
|
25
|
+
* or null for a refused or absent one.
|
|
26
|
+
*
|
|
27
|
+
* Settled once per request, before anything can crash, and handed to every
|
|
28
|
+
* answer the request ends in: the crash path then applies a verdict already
|
|
29
|
+
* reached and runs no app code of its own. A predicate that throws counts as
|
|
30
|
+
* refused and is logged. `new URL(origin)` throws on the `Origin: null` a
|
|
31
|
+
* sandboxed frame or a cross-origin redirect sends, and a CORS header is no
|
|
32
|
+
* reason to fail the request it decorates.
|
|
33
|
+
*/
|
|
34
|
+
export declare const allowedCorsOriginOf: (config: LambderCorsConfig, ctx: LambderRenderContext) => string | null;
|
|
35
|
+
/** Mutate the response with the CORS headers the config allows, for the origin verdict allowedCorsOriginOf settled for this request. */
|
|
36
|
+
export declare const applyCorsHeaders: (config: LambderCorsConfig | null, allowedOrigin: string | null, response: LambderResponse, isPreflight: boolean) => void;
|
package/dist/core/LambderCors.js
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The Access-Control-Allow-Origin this request earns: "*", the echoed origin,
|
|
3
|
+
* or null for a refused or absent one.
|
|
4
|
+
*
|
|
5
|
+
* Settled once per request, before anything can crash, and handed to every
|
|
6
|
+
* answer the request ends in: the crash path then applies a verdict already
|
|
7
|
+
* reached and runs no app code of its own. A predicate that throws counts as
|
|
8
|
+
* refused and is logged. `new URL(origin)` throws on the `Origin: null` a
|
|
9
|
+
* sandboxed frame or a cross-origin redirect sends, and a CORS header is no
|
|
10
|
+
* reason to fail the request it decorates.
|
|
11
|
+
*/
|
|
12
|
+
export const allowedCorsOriginOf = (config, ctx) => {
|
|
6
13
|
const origins = config.origins ?? "*";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
if (origins === "*")
|
|
15
|
+
return "*";
|
|
16
|
+
const origin = ctx.header("origin");
|
|
17
|
+
if (!origin)
|
|
18
|
+
return null;
|
|
19
|
+
if (Array.isArray(origins))
|
|
20
|
+
return origins.includes(origin) ? origin : null;
|
|
21
|
+
try {
|
|
22
|
+
return origins(origin, ctx) ? origin : null;
|
|
10
23
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
else {
|
|
15
|
-
allowOrigin = origin && origins(origin, ctx) ? origin : null;
|
|
24
|
+
catch (predicateErr) {
|
|
25
|
+
console.error("Lambder: cors.origins threw; the origin was refused.", predicateErr);
|
|
26
|
+
return null;
|
|
16
27
|
}
|
|
17
|
-
|
|
28
|
+
};
|
|
29
|
+
/** Mutate the response with the CORS headers the config allows, for the origin verdict allowedCorsOriginOf settled for this request. */
|
|
30
|
+
export const applyCorsHeaders = (config, allowedOrigin, response, isPreflight) => {
|
|
31
|
+
if (!config)
|
|
18
32
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
// Under an allowlist or a predicate the answer depends on Origin even when
|
|
34
|
+
// this one was refused: a cache must not serve the answer to a refused or
|
|
35
|
+
// absent Origin to an allowed one, which would read it as a CORS failure.
|
|
36
|
+
if ((config.origins ?? "*") !== "*")
|
|
21
37
|
response.addHeader("Vary", "Origin");
|
|
38
|
+
if (!allowedOrigin)
|
|
39
|
+
return;
|
|
40
|
+
response.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
22
41
|
if (config.credentials)
|
|
23
42
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
|
24
43
|
if (isPreflight) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
+
import type { LambderCrashOptions, LambderCrashSite } from "./LambderCreateOptions.js";
|
|
3
|
+
import { LambderResponse } from "./LambderResponse.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `crashes` option applied: what the instance does with a crash beyond
|
|
6
|
+
* handing it to the app's global error handler.
|
|
7
|
+
*
|
|
8
|
+
* Kept apart from the request path, like CORS and file serving: a crash is
|
|
9
|
+
* reported wherever it happened (an API call, a route, an event, a `created`
|
|
10
|
+
* hook) and answered by the framework only when nothing the app wrote
|
|
11
|
+
* answered it, so neither is a step of rendering a request.
|
|
12
|
+
*/
|
|
13
|
+
export declare class LambderCrashHandling {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private readonly apiVersion;
|
|
16
|
+
private readonly reportTimeoutMs;
|
|
17
|
+
constructor(options: LambderCrashOptions, apiVersion: string | null);
|
|
18
|
+
/**
|
|
19
|
+
* Hands a crash to the app's reporter and waits for it, for up to
|
|
20
|
+
* reportTimeoutMs. Never throws: a reporter that fails is logged beside
|
|
21
|
+
* the crash it was given, one that has not finished by then is logged
|
|
22
|
+
* with it as unfinished, and either way the request goes on to be
|
|
23
|
+
* answered. The report itself cannot be cancelled and runs on; only the
|
|
24
|
+
* wait ends.
|
|
25
|
+
*/
|
|
26
|
+
report(error: Error, site: LambderCrashSite): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* The framework's own 500, for a crash nothing the app wrote answered:
|
|
29
|
+
* API calls get the core's crash envelope so clients can parse a
|
|
30
|
+
* structured failure, everything else plain text. It carries the crash in
|
|
31
|
+
* full only for a caller `crashes.reveal` trusts.
|
|
32
|
+
*
|
|
33
|
+
* Without a reporter it also logs the crash (and a global error handler
|
|
34
|
+
* that threw on it): the invocation answered, so Lambda counts no error,
|
|
35
|
+
* and this log line is the only trace the crash leaves.
|
|
36
|
+
*/
|
|
37
|
+
frameworkResponse(error: Error, ctx: LambderRenderContext | null, errorHandlerCrash: Error | null): Promise<LambderResponse>;
|
|
38
|
+
/** Whether this request's caller may read the crash; a reveal rule that throws answers no. */
|
|
39
|
+
private mayReveal;
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { crashAnswer } from "../api/LambderApiEnvelope.js";
|
|
2
|
+
import { describeCrash } from "../shared/wire/LambderCrashDetail.js";
|
|
3
|
+
import { stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
|
|
4
|
+
import { LambderResponse, responseFromAnswer } from "./LambderResponse.js";
|
|
5
|
+
/**
|
|
6
|
+
* How long a crash's answer waits for the reporter by default: long enough
|
|
7
|
+
* for a network write to a log or an error tracker, short enough that a
|
|
8
|
+
* stalled one leaves most of a function's timeout to answer in.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CRASH_REPORT_TIMEOUT_MS = 3_000;
|
|
11
|
+
/**
|
|
12
|
+
* The `crashes` option applied: what the instance does with a crash beyond
|
|
13
|
+
* handing it to the app's global error handler.
|
|
14
|
+
*
|
|
15
|
+
* Kept apart from the request path, like CORS and file serving: a crash is
|
|
16
|
+
* reported wherever it happened (an API call, a route, an event, a `created`
|
|
17
|
+
* hook) and answered by the framework only when nothing the app wrote
|
|
18
|
+
* answered it, so neither is a step of rendering a request.
|
|
19
|
+
*/
|
|
20
|
+
export class LambderCrashHandling {
|
|
21
|
+
options;
|
|
22
|
+
apiVersion;
|
|
23
|
+
reportTimeoutMs;
|
|
24
|
+
constructor(options, apiVersion) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.apiVersion = apiVersion;
|
|
27
|
+
this.reportTimeoutMs = options.reportTimeoutMs ?? DEFAULT_CRASH_REPORT_TIMEOUT_MS;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hands a crash to the app's reporter and waits for it, for up to
|
|
31
|
+
* reportTimeoutMs. Never throws: a reporter that fails is logged beside
|
|
32
|
+
* the crash it was given, one that has not finished by then is logged
|
|
33
|
+
* with it as unfinished, and either way the request goes on to be
|
|
34
|
+
* answered. The report itself cannot be cancelled and runs on; only the
|
|
35
|
+
* wait ends.
|
|
36
|
+
*/
|
|
37
|
+
async report(error, site) {
|
|
38
|
+
const report = this.options.report;
|
|
39
|
+
if (!report)
|
|
40
|
+
return;
|
|
41
|
+
const deadline = AbortSignal.timeout(this.reportTimeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
// Started inside the promise chain, so a reporter that throws
|
|
44
|
+
// before its first await lands in the catch below like one that
|
|
45
|
+
// rejects.
|
|
46
|
+
await stopWaitingWhenAborted(Promise.resolve().then(() => report(error, site)), deadline);
|
|
47
|
+
}
|
|
48
|
+
catch (reportErr) {
|
|
49
|
+
if (deadline.aborted && reportErr === deadline.reason) {
|
|
50
|
+
console.error(`Lambder: crashes.report did not finish within ${this.reportTimeoutMs} ms and is no longer waited for. The crash:`, error);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error("Lambder: crashes.report threw while reporting a crash. The crash:", error, "What the reporter threw:", reportErr);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The framework's own 500, for a crash nothing the app wrote answered:
|
|
59
|
+
* API calls get the core's crash envelope so clients can parse a
|
|
60
|
+
* structured failure, everything else plain text. It carries the crash in
|
|
61
|
+
* full only for a caller `crashes.reveal` trusts.
|
|
62
|
+
*
|
|
63
|
+
* Without a reporter it also logs the crash (and a global error handler
|
|
64
|
+
* that threw on it): the invocation answered, so Lambda counts no error,
|
|
65
|
+
* and this log line is the only trace the crash leaves.
|
|
66
|
+
*/
|
|
67
|
+
async frameworkResponse(error, ctx, errorHandlerCrash) {
|
|
68
|
+
if (!this.options.report) {
|
|
69
|
+
console.error(`Lambder: ${ctx ? `${ctx.method} ${ctx.path}` : "a request"} crashed and was answered with the framework's 500.`, error);
|
|
70
|
+
if (errorHandlerCrash)
|
|
71
|
+
console.error(errorHandlerCrash);
|
|
72
|
+
}
|
|
73
|
+
const revealed = ctx && await this.mayReveal(ctx) ? describeCrash(error, ctx) : null;
|
|
74
|
+
if (ctx?.api) {
|
|
75
|
+
return responseFromAnswer(crashAnswer(this.apiVersion, revealed ? { crash: revealed, logList: ctx.logList } : undefined));
|
|
76
|
+
}
|
|
77
|
+
return new LambderResponse({
|
|
78
|
+
statusCode: 500,
|
|
79
|
+
body: revealed
|
|
80
|
+
? ["Internal Server Error.", "", revealed.stack ?? `${revealed.name}: ${revealed.message}`,
|
|
81
|
+
...(revealed.causeList ?? []).map((cause) => `Caused by: ${cause.stack ?? `${cause.name}: ${cause.message}`}`)].join("\n")
|
|
82
|
+
: "Internal Server Error.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Whether this request's caller may read the crash; a reveal rule that throws answers no. */
|
|
86
|
+
async mayReveal(ctx) {
|
|
87
|
+
if (!this.options.reveal)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
return (await this.options.reveal(ctx)) === true;
|
|
91
|
+
}
|
|
92
|
+
catch (revealErr) {
|
|
93
|
+
console.error("Lambder: crashes.reveal threw; the crash was not revealed.", revealErr);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|