lambder 6.0.2 → 7.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 +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an invoke call reports: the failure reasons, the outcome union, the
|
|
3
|
+
* error api() throws, and the pure functions that read a failure (what a
|
|
4
|
+
* rejected delivery means, what Lambda's error payload says, the one-line
|
|
5
|
+
* detail an error message ends with).
|
|
6
|
+
*
|
|
7
|
+
* Split out of LambderInvokeCaller for the reason shared/wire/LambderApiOutcome.ts
|
|
8
|
+
* is split out of the browser caller: this is the vocabulary a CALLER of the
|
|
9
|
+
* caller reads, and a site that only annotates an outcome or narrows an error
|
|
10
|
+
* should not have to read a 700-line class to find it. The functions here
|
|
11
|
+
* touch none of the caller's state, so every "what went wrong on an invoke"
|
|
12
|
+
* answer is in one file.
|
|
13
|
+
*/
|
|
14
|
+
import type { LambderApiEnvelopeBody } from "../shared/wire/LambderApiContract.js";
|
|
15
|
+
import type { LambderApiFailureReason, LambderValidationError } from "../shared/wire/LambderApiOutcome.js";
|
|
16
|
+
import type { LambderCrashDetail } from "../shared/wire/LambderCrashDetail.js";
|
|
17
|
+
export type LambderInvokeFailureReason = LambderApiFailureReason | 'crash' | 'protocol' | 'payloadTooLarge';
|
|
18
|
+
/** Lambda's own error payload for a FunctionError invocation. */
|
|
19
|
+
export type LambderInvokeFunctionError = {
|
|
20
|
+
errorType?: string;
|
|
21
|
+
errorMessage?: string;
|
|
22
|
+
trace?: string[];
|
|
23
|
+
};
|
|
24
|
+
/** What every invoke failure carries, whatever went wrong. */
|
|
25
|
+
type LambderInvokeFailureFields = {
|
|
26
|
+
ok: false;
|
|
27
|
+
/** HTTP status, when the callee answered. */
|
|
28
|
+
status?: number;
|
|
29
|
+
/** Envelope errorMessage, when the callee provided one. */
|
|
30
|
+
errorMessage?: any;
|
|
31
|
+
/** Seconds to wait before retrying, from the answer's Retry-After header. */
|
|
32
|
+
retryAfterSeconds?: number;
|
|
33
|
+
/** Always present: the error api() throws for this failure, with the callee's error as its cause when one is known. */
|
|
34
|
+
error: LambderInvokeError;
|
|
35
|
+
/** The answer's logList; empty when no envelope came back. */
|
|
36
|
+
logList: unknown[];
|
|
37
|
+
/** The answer's Set-Cookie values; empty when no answer came back. */
|
|
38
|
+
cookies: string[];
|
|
39
|
+
};
|
|
40
|
+
/** HTTP 422: the callee rejected the input against the API's schema. Always carries the issues. */
|
|
41
|
+
export type LambderInvokeValidationFailure = LambderInvokeFailureFields & {
|
|
42
|
+
reason: 'validation';
|
|
43
|
+
zodError: LambderValidationError;
|
|
44
|
+
};
|
|
45
|
+
/** Lambda reported a FunctionError: the callee failed outside the framework (an init failure, a timeout, out of memory). Always carries the runtime's error payload. */
|
|
46
|
+
export type LambderInvokeCrashFailure = LambderInvokeFailureFields & {
|
|
47
|
+
reason: 'crash';
|
|
48
|
+
functionError: LambderInvokeFunctionError;
|
|
49
|
+
};
|
|
50
|
+
/** Refused before sending: the serialized event is over the invoke cap. Always carries its size. */
|
|
51
|
+
export type LambderInvokePayloadTooLargeFailure = LambderInvokeFailureFields & {
|
|
52
|
+
reason: 'payloadTooLarge';
|
|
53
|
+
/** The event's byte size, so a caller can say by how much it is over. */
|
|
54
|
+
bytes: number;
|
|
55
|
+
};
|
|
56
|
+
/** The callee answered, and the envelope itself says the call is refused. Always carries that envelope. */
|
|
57
|
+
export type LambderInvokeEnvelopeFailure = LambderInvokeFailureFields & {
|
|
58
|
+
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
|
|
59
|
+
response: LambderApiEnvelopeBody<any>;
|
|
60
|
+
/** The callee's crash detail, when its global error handler sent one (the envelope's `crash` field). */
|
|
61
|
+
crash?: LambderCrashDetail;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Nothing usable came back: the invoke never arrived or was given up on, the
|
|
65
|
+
* Lambda service answered instead of the callee, the callee answered 5xx, or
|
|
66
|
+
* something around the call threw. A 5xx carries `response` when the callee
|
|
67
|
+
* answered with Lambder's own envelope, which is how a crash detail and a
|
|
68
|
+
* logList arrive with it.
|
|
69
|
+
*/
|
|
70
|
+
export type LambderInvokeDeliveryFailure = LambderInvokeFailureFields & {
|
|
71
|
+
reason: 'network' | 'timeout' | 'server' | 'protocol' | 'unknown';
|
|
72
|
+
response?: LambderApiEnvelopeBody<any>;
|
|
73
|
+
/** The callee's crash detail, when its global error handler sent one (the envelope's `crash` field). */
|
|
74
|
+
crash?: LambderCrashDetail;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* A failed invoke, discriminated by `reason` rather than written as one arm of
|
|
78
|
+
* optional fields, so narrowing to a reason narrows to what that reason
|
|
79
|
+
* actually carries: `zodError` after `validation`, `functionError` after
|
|
80
|
+
* `crash`, `bytes` after `payloadTooLarge`, `response` after an envelope
|
|
81
|
+
* reason, so a reader never writes an optional chain or a `!` for a field the
|
|
82
|
+
* reason already guarantees. The browser caller's LambderApiOutcome is
|
|
83
|
+
* discriminated the same way.
|
|
84
|
+
*
|
|
85
|
+
* `error`, `logList` and `cookies` are on every arm, and `status`,
|
|
86
|
+
* `errorMessage` and `retryAfterSeconds` are there whenever an answer came
|
|
87
|
+
* back to read them from.
|
|
88
|
+
*/
|
|
89
|
+
export type LambderInvokeFailure = LambderInvokeValidationFailure | LambderInvokeCrashFailure | LambderInvokePayloadTooLargeFailure | LambderInvokeEnvelopeFailure | LambderInvokeDeliveryFailure;
|
|
90
|
+
export type LambderInvokeOutcome<T> = {
|
|
91
|
+
ok: true;
|
|
92
|
+
payload: T;
|
|
93
|
+
response: LambderApiEnvelopeBody<T>;
|
|
94
|
+
logList: unknown[];
|
|
95
|
+
/**
|
|
96
|
+
* The answer's Set-Cookie values. A caller carrying a user's session
|
|
97
|
+
* is the browser for that call, and nothing else is: a callee that
|
|
98
|
+
* rotated or cleared the session cookies says so here, and a caller
|
|
99
|
+
* that ignores them keeps sending the old token. See
|
|
100
|
+
* reissueSession() on the callee for the tokens themselves.
|
|
101
|
+
*/
|
|
102
|
+
cookies: string[];
|
|
103
|
+
} | LambderInvokeFailure;
|
|
104
|
+
export type LambderInvokeErrorInit = {
|
|
105
|
+
message: string;
|
|
106
|
+
reason: LambderInvokeFailureReason;
|
|
107
|
+
apiName: string;
|
|
108
|
+
functionName: string;
|
|
109
|
+
status?: number;
|
|
110
|
+
errorMessage?: any;
|
|
111
|
+
crash?: LambderCrashDetail;
|
|
112
|
+
functionError?: LambderInvokeFunctionError;
|
|
113
|
+
logList: unknown[];
|
|
114
|
+
zodError?: LambderValidationError;
|
|
115
|
+
retryAfterSeconds?: number;
|
|
116
|
+
bytes?: number;
|
|
117
|
+
cause?: unknown;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* What api() throws. Its message names the function, the API and the reason,
|
|
121
|
+
* so an error reporter that fingerprints on the message groups one broken
|
|
122
|
+
* API into one row; its cause is the callee's own error rebuilt from the
|
|
123
|
+
* crash detail (or Lambda's FunctionError, or the SDK's rejection), so a
|
|
124
|
+
* reporter that walks causes stores the callee's stack.
|
|
125
|
+
*/
|
|
126
|
+
export declare class LambderInvokeError extends Error {
|
|
127
|
+
/** Brand for detection across duplicate lambder installs, like LambderApiRefusal. */
|
|
128
|
+
readonly isLambderInvokeError = true;
|
|
129
|
+
readonly reason: LambderInvokeFailureReason;
|
|
130
|
+
readonly apiName: string;
|
|
131
|
+
readonly functionName: string;
|
|
132
|
+
readonly status?: number;
|
|
133
|
+
readonly errorMessage?: any;
|
|
134
|
+
readonly crash?: LambderCrashDetail;
|
|
135
|
+
readonly functionError?: LambderInvokeFunctionError;
|
|
136
|
+
readonly logList: unknown[];
|
|
137
|
+
readonly zodError?: LambderValidationError;
|
|
138
|
+
readonly retryAfterSeconds?: number;
|
|
139
|
+
readonly bytes?: number;
|
|
140
|
+
/** The full failure outcome; it carries this error and this error carries it. */
|
|
141
|
+
outcome: LambderInvokeFailure;
|
|
142
|
+
constructor(init: LambderInvokeErrorInit);
|
|
143
|
+
}
|
|
144
|
+
/** Brand-based type guard (see LambderInvokeError.isLambderInvokeError). */
|
|
145
|
+
export declare const isLambderInvokeError: (err: unknown) => err is LambderInvokeError;
|
|
146
|
+
/**
|
|
147
|
+
* What a rejected delivery means, as far as the rejection itself says.
|
|
148
|
+
*
|
|
149
|
+
* A transport that named its own reason is believed. Otherwise: the Lambda
|
|
150
|
+
* SDK throws its service exceptions (AccessDeniedException,
|
|
151
|
+
* ResourceNotFoundException, RequestEntityTooLargeException and the rest)
|
|
152
|
+
* with a $fault mark and a name ending in "Exception", while a connectivity
|
|
153
|
+
* failure arrives as a plain Error or TypeError carrying neither. The first
|
|
154
|
+
* kind is the Lambda service answering the invoke, which is a permission,
|
|
155
|
+
* wiring or size fault to go and fix; calling it `network` sent whoever read
|
|
156
|
+
* it to look at their connection instead.
|
|
157
|
+
*/
|
|
158
|
+
export declare const classifyDeliveryFailure: (error: Error) => "network" | "protocol";
|
|
159
|
+
/** Lambda's error payload as an Error, with the callee's own name and trace. */
|
|
160
|
+
export declare const errorFromFunctionError: (functionError: LambderInvokeFunctionError) => Error;
|
|
161
|
+
/** Lambda's error payload as it actually arrives: an object with the three fields, or whatever else the runtime sent. */
|
|
162
|
+
export declare const parseFunctionError: (result: unknown) => LambderInvokeFunctionError;
|
|
163
|
+
/** The one-line detail a failure's message ends with. */
|
|
164
|
+
export declare const describeFailure: (init: Omit<LambderInvokeErrorInit, "message" | "apiName" | "functionName" | "logList">) => string;
|
|
165
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an invoke call reports: the failure reasons, the outcome union, the
|
|
3
|
+
* error api() throws, and the pure functions that read a failure (what a
|
|
4
|
+
* rejected delivery means, what Lambda's error payload says, the one-line
|
|
5
|
+
* detail an error message ends with).
|
|
6
|
+
*
|
|
7
|
+
* Split out of LambderInvokeCaller for the reason shared/wire/LambderApiOutcome.ts
|
|
8
|
+
* is split out of the browser caller: this is the vocabulary a CALLER of the
|
|
9
|
+
* caller reads, and a site that only annotates an outcome or narrows an error
|
|
10
|
+
* should not have to read a 700-line class to find it. The functions here
|
|
11
|
+
* touch none of the caller's state, so every "what went wrong on an invoke"
|
|
12
|
+
* answer is in one file.
|
|
13
|
+
*/
|
|
14
|
+
import { isLambderTransportFailure } from "../shared/transport/LambderApiTransport.js";
|
|
15
|
+
/**
|
|
16
|
+
* What api() throws. Its message names the function, the API and the reason,
|
|
17
|
+
* so an error reporter that fingerprints on the message groups one broken
|
|
18
|
+
* API into one row; its cause is the callee's own error rebuilt from the
|
|
19
|
+
* crash detail (or Lambda's FunctionError, or the SDK's rejection), so a
|
|
20
|
+
* reporter that walks causes stores the callee's stack.
|
|
21
|
+
*/
|
|
22
|
+
export class LambderInvokeError extends Error {
|
|
23
|
+
/** Brand for detection across duplicate lambder installs, like LambderApiRefusal. */
|
|
24
|
+
isLambderInvokeError = true;
|
|
25
|
+
reason;
|
|
26
|
+
apiName;
|
|
27
|
+
functionName;
|
|
28
|
+
status;
|
|
29
|
+
errorMessage;
|
|
30
|
+
crash;
|
|
31
|
+
functionError;
|
|
32
|
+
logList;
|
|
33
|
+
zodError;
|
|
34
|
+
retryAfterSeconds;
|
|
35
|
+
bytes;
|
|
36
|
+
/** The full failure outcome; it carries this error and this error carries it. */
|
|
37
|
+
outcome;
|
|
38
|
+
constructor(init) {
|
|
39
|
+
super(init.message, init.cause !== undefined ? { cause: init.cause } : undefined);
|
|
40
|
+
this.name = "LambderInvokeError";
|
|
41
|
+
this.reason = init.reason;
|
|
42
|
+
this.apiName = init.apiName;
|
|
43
|
+
this.functionName = init.functionName;
|
|
44
|
+
this.status = init.status;
|
|
45
|
+
this.errorMessage = init.errorMessage;
|
|
46
|
+
this.crash = init.crash;
|
|
47
|
+
this.functionError = init.functionError;
|
|
48
|
+
this.logList = init.logList;
|
|
49
|
+
this.zodError = init.zodError;
|
|
50
|
+
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
51
|
+
this.bytes = init.bytes;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Brand-based type guard (see LambderInvokeError.isLambderInvokeError). */
|
|
55
|
+
export const isLambderInvokeError = (err) => err instanceof Error && err.isLambderInvokeError === true;
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Reading a failure: pure functions over what came back, used by the caller.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* What a rejected delivery means, as far as the rejection itself says.
|
|
61
|
+
*
|
|
62
|
+
* A transport that named its own reason is believed. Otherwise: the Lambda
|
|
63
|
+
* SDK throws its service exceptions (AccessDeniedException,
|
|
64
|
+
* ResourceNotFoundException, RequestEntityTooLargeException and the rest)
|
|
65
|
+
* with a $fault mark and a name ending in "Exception", while a connectivity
|
|
66
|
+
* failure arrives as a plain Error or TypeError carrying neither. The first
|
|
67
|
+
* kind is the Lambda service answering the invoke, which is a permission,
|
|
68
|
+
* wiring or size fault to go and fix; calling it `network` sent whoever read
|
|
69
|
+
* it to look at their connection instead.
|
|
70
|
+
*/
|
|
71
|
+
export const classifyDeliveryFailure = (error) => {
|
|
72
|
+
if (isLambderTransportFailure(error))
|
|
73
|
+
return error.reason;
|
|
74
|
+
const fault = error.$fault;
|
|
75
|
+
if (fault === "client" || fault === "server")
|
|
76
|
+
return 'protocol';
|
|
77
|
+
return error.name.endsWith("Exception") ? 'protocol' : 'network';
|
|
78
|
+
};
|
|
79
|
+
/** Lambda's error payload as an Error, with the callee's own name and trace. */
|
|
80
|
+
export const errorFromFunctionError = (functionError) => {
|
|
81
|
+
const error = new Error(functionError.errorMessage ?? "the function failed");
|
|
82
|
+
error.name = functionError.errorType ?? "FunctionError";
|
|
83
|
+
if (functionError.trace?.length)
|
|
84
|
+
error.stack = functionError.trace.join("\n");
|
|
85
|
+
return error;
|
|
86
|
+
};
|
|
87
|
+
/** Lambda's error payload as it actually arrives: an object with the three fields, or whatever else the runtime sent. */
|
|
88
|
+
export const parseFunctionError = (result) => {
|
|
89
|
+
if (result && typeof result === "object") {
|
|
90
|
+
const { errorType, errorMessage, trace } = result;
|
|
91
|
+
return {
|
|
92
|
+
...(typeof errorType === "string" ? { errorType } : {}),
|
|
93
|
+
...(typeof errorMessage === "string" ? { errorMessage } : {}),
|
|
94
|
+
...(Array.isArray(trace) ? { trace: trace.map(String) } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { errorMessage: typeof result === "string" ? result : undefined };
|
|
98
|
+
};
|
|
99
|
+
/** The one-line detail a failure's message ends with. */
|
|
100
|
+
export const describeFailure = (init) => {
|
|
101
|
+
if (init.crash)
|
|
102
|
+
return init.crash.message;
|
|
103
|
+
if (init.functionError)
|
|
104
|
+
return `${init.functionError.errorType ?? "FunctionError"}: ${init.functionError.errorMessage ?? "the function failed"}`;
|
|
105
|
+
if (init.errorMessage !== undefined) {
|
|
106
|
+
const content = init.errorMessage?.content;
|
|
107
|
+
if (typeof content === "string")
|
|
108
|
+
return content;
|
|
109
|
+
if (typeof init.errorMessage === "string")
|
|
110
|
+
return init.errorMessage;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.stringify(init.errorMessage);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return String(init.errorMessage);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (init.reason === 'validation')
|
|
119
|
+
return "the callee rejected the input";
|
|
120
|
+
if (init.reason === 'versionExpired')
|
|
121
|
+
return "the callee answered versionExpired";
|
|
122
|
+
if (init.reason === 'sessionExpired')
|
|
123
|
+
return "the callee answered sessionExpired";
|
|
124
|
+
if (init.reason === 'notAuthorized')
|
|
125
|
+
return "the callee answered notAuthorized";
|
|
126
|
+
if (init.cause instanceof Error)
|
|
127
|
+
return init.cause.message;
|
|
128
|
+
return init.status !== undefined ? `HTTP ${init.status}` : "no answer";
|
|
129
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two conversions between a request and what Lambda speaks: a request
|
|
3
|
+
* as the API Gateway payload-format-2.0 event it would have delivered, and
|
|
4
|
+
* a function's answer as the decoded HTTP result it stands for. Shared by
|
|
5
|
+
* LambderInvokeCaller (a real invoke through the Lambda SDK) and
|
|
6
|
+
* lambderHandlerTransport (a Lambder handler called in-process, for tests),
|
|
7
|
+
* along with the smaller conversions that belong to a synthesized request:
|
|
8
|
+
* its body envelope, and a carried session as the cookie pair it travels as.
|
|
9
|
+
* Server-only: Buffer and the codec's zlib restore.
|
|
10
|
+
*/
|
|
11
|
+
import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
|
|
12
|
+
import type { LambderCompressedBrotliPayload, LambderCompressedGzipPayload } from "../shared/wire/LambderRequestPayload.js";
|
|
13
|
+
/** Marks a synthesized request as an invoke, for guards and hooks that want to tell. Not an authorization. */
|
|
14
|
+
export declare const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
|
|
15
|
+
/** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
|
|
16
|
+
export declare const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
|
|
17
|
+
/** The value of the marker header; a future incompatible event shape would bump it. */
|
|
18
|
+
export declare const LAMBDER_INVOKE_PROTOCOL = "1";
|
|
19
|
+
/** A session carried on a user's behalf: the two values a browser holds. */
|
|
20
|
+
export type LambderInvokeSession = {
|
|
21
|
+
token: string;
|
|
22
|
+
csrf: string;
|
|
23
|
+
};
|
|
24
|
+
/** The session's cookie pair for a synthesized event: the token rides as a cookie, the CSRF value in the envelope's `token` field. */
|
|
25
|
+
export declare const sessionCookies: (session: LambderInvokeSession | undefined, tokenCookieKey: string) => string[] | undefined;
|
|
26
|
+
export type LambderSynthesizedRequest = {
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
query?: Record<string, string>;
|
|
30
|
+
host: string;
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
clientIp?: string;
|
|
33
|
+
cookies?: string[];
|
|
34
|
+
body?: string | Buffer;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The payload-format-2.0 event API Gateway would deliver for this request.
|
|
38
|
+
* `invoke: true` adds the invoke marker headers a server-to-server call
|
|
39
|
+
* carries; a browser-shaped request (the handler transport) leaves them off.
|
|
40
|
+
*
|
|
41
|
+
* The client address is `clientIp` and reaches the callee as
|
|
42
|
+
* requestContext.http.sourceIp only. Writing it as x-forwarded-for as well
|
|
43
|
+
* would put the same fact on a channel a callee may be configured to trust
|
|
44
|
+
* (trustedClientIpHeaders), and the header is the one the caller's own
|
|
45
|
+
* `headers` could otherwise have set.
|
|
46
|
+
*/
|
|
47
|
+
export declare const synthesizeLambdaHttpEvent: (request: LambderSynthesizedRequest, options: {
|
|
48
|
+
invoke: boolean;
|
|
49
|
+
}) => APIGatewayProxyEventV2;
|
|
50
|
+
/**
|
|
51
|
+
* The body envelope LambderCaller sends, minus the fields only a browser has
|
|
52
|
+
* a value for, as JSON. A plain payload arrives already serialized (the
|
|
53
|
+
* compression decision needed its JSON) and is spliced in rather than
|
|
54
|
+
* parsed and stringified a second time; a compressed one rides as its two
|
|
55
|
+
* fields.
|
|
56
|
+
*/
|
|
57
|
+
export declare const buildEnvelopeJson: (fields: {
|
|
58
|
+
apiName: string;
|
|
59
|
+
version?: string;
|
|
60
|
+
csrf?: string;
|
|
61
|
+
siteHost: string;
|
|
62
|
+
/** The payload's own JSON, when it goes plainly. */
|
|
63
|
+
payloadJson?: string;
|
|
64
|
+
/** The compressed pair, when it goes compressed. */
|
|
65
|
+
compressed?: LambderCompressedBrotliPayload | LambderCompressedGzipPayload | null;
|
|
66
|
+
guardInputs?: Record<string, unknown>;
|
|
67
|
+
idempotencyKey?: string;
|
|
68
|
+
}) => string;
|
|
69
|
+
/** The decoded HTTP answer of a Lambda result: status, lowercased headers, cookies and the body bytes (decompressed when the callee compressed them). */
|
|
70
|
+
export type LambderLambdaHttpResult = {
|
|
71
|
+
statusCode: number;
|
|
72
|
+
headers: Record<string, string>;
|
|
73
|
+
cookies: string[];
|
|
74
|
+
body: Buffer;
|
|
75
|
+
text: () => string;
|
|
76
|
+
json: () => unknown;
|
|
77
|
+
};
|
|
78
|
+
/** The function's answer as an HTTP result; throws when it is not one, or its compressed body cannot be restored under `maxBodyBytes`. */
|
|
79
|
+
export declare const decodeLambdaHttpResult: (result: unknown, maxBodyBytes: number) => Promise<LambderLambdaHttpResult>;
|
|
80
|
+
/** A Lambda context for an in-process call, the fields a handler might read filled plausibly. */
|
|
81
|
+
export declare const localLambdaContext: (functionName: string, overrides?: Partial<Context>) => Context;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two conversions between a request and what Lambda speaks: a request
|
|
3
|
+
* as the API Gateway payload-format-2.0 event it would have delivered, and
|
|
4
|
+
* a function's answer as the decoded HTTP result it stands for. Shared by
|
|
5
|
+
* LambderInvokeCaller (a real invoke through the Lambda SDK) and
|
|
6
|
+
* lambderHandlerTransport (a Lambder handler called in-process, for tests),
|
|
7
|
+
* along with the smaller conversions that belong to a synthesized request:
|
|
8
|
+
* its body envelope, and a carried session as the cookie pair it travels as.
|
|
9
|
+
* Server-only: Buffer and the codec's zlib restore.
|
|
10
|
+
*/
|
|
11
|
+
import { restoreBytes } from "../shared/wire/LambderCompressionCodec.js";
|
|
12
|
+
import { bytesToBase64 } from "../shared/util/LambderBase64.js";
|
|
13
|
+
import { buildEnvelopeFields } from "../shared/transport/LambderApiTransport.js";
|
|
14
|
+
/** Marks a synthesized request as an invoke, for guards and hooks that want to tell. Not an authorization. */
|
|
15
|
+
export const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
|
|
16
|
+
/** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
|
|
17
|
+
export const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
|
|
18
|
+
/** The value of the marker header; a future incompatible event shape would bump it. */
|
|
19
|
+
export const LAMBDER_INVOKE_PROTOCOL = "1";
|
|
20
|
+
/**
|
|
21
|
+
* The forwarded-address header a gateway writes. This event never writes it:
|
|
22
|
+
* the address it asserts travels in requestContext.http.sourceIp, which is
|
|
23
|
+
* what resolveClientIp reads and the only channel a callee trusts by default.
|
|
24
|
+
*/
|
|
25
|
+
const FORWARDED_FOR_HEADER = "x-forwarded-for";
|
|
26
|
+
/** The session's cookie pair for a synthesized event: the token rides as a cookie, the CSRF value in the envelope's `token` field. */
|
|
27
|
+
export const sessionCookies = (session, tokenCookieKey) => session ? [`${tokenCookieKey}=${session.token}`] : undefined;
|
|
28
|
+
const randomRequestId = () => {
|
|
29
|
+
const webCrypto = globalThis.crypto;
|
|
30
|
+
if (webCrypto?.randomUUID)
|
|
31
|
+
return webCrypto.randomUUID();
|
|
32
|
+
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The payload-format-2.0 event API Gateway would deliver for this request.
|
|
36
|
+
* `invoke: true` adds the invoke marker headers a server-to-server call
|
|
37
|
+
* carries; a browser-shaped request (the handler transport) leaves them off.
|
|
38
|
+
*
|
|
39
|
+
* The client address is `clientIp` and reaches the callee as
|
|
40
|
+
* requestContext.http.sourceIp only. Writing it as x-forwarded-for as well
|
|
41
|
+
* would put the same fact on a channel a callee may be configured to trust
|
|
42
|
+
* (trustedClientIpHeaders), and the header is the one the caller's own
|
|
43
|
+
* `headers` could otherwise have set.
|
|
44
|
+
*/
|
|
45
|
+
export const synthesizeLambdaHttpEvent = (request, options) => {
|
|
46
|
+
// The caller's own headers go on first, so the ones this function owns
|
|
47
|
+
// cannot be displaced by them. Forwarding an incoming browser request's
|
|
48
|
+
// headers into `headers` is an ordinary gateway-lambda pattern, and with
|
|
49
|
+
// the spread last it let that end user overwrite the invoke markers and
|
|
50
|
+
// the forwarded address this event is asserting.
|
|
51
|
+
const headers = {};
|
|
52
|
+
for (const [key, value] of Object.entries(request.headers ?? {}))
|
|
53
|
+
headers[key.toLowerCase()] = value;
|
|
54
|
+
// These three the event owns unconditionally, whatever the caller passed:
|
|
55
|
+
// a forwarded address the caller did not assert through `clientIp` is not
|
|
56
|
+
// this event's to make, and the invoke markers say what this event is. A
|
|
57
|
+
// gateway lambda that forwards a browser's headers wholesale would
|
|
58
|
+
// otherwise hand a callee that trusts x-forwarded-for an end-user-chosen
|
|
59
|
+
// ctx.ip, and let a browser-shaped request claim to be an invoke.
|
|
60
|
+
delete headers[FORWARDED_FOR_HEADER];
|
|
61
|
+
delete headers[LAMBDER_INVOKE_HEADER];
|
|
62
|
+
delete headers[LAMBDER_INVOKED_BY_HEADER];
|
|
63
|
+
headers.host = request.host;
|
|
64
|
+
headers["accept-encoding"] = "br, gzip";
|
|
65
|
+
if (options.invoke) {
|
|
66
|
+
headers[LAMBDER_INVOKE_HEADER] = LAMBDER_INVOKE_PROTOCOL;
|
|
67
|
+
const invokedBy = typeof process !== "undefined" ? process.env?.AWS_LAMBDA_FUNCTION_NAME : undefined;
|
|
68
|
+
if (invokedBy)
|
|
69
|
+
headers[LAMBDER_INVOKED_BY_HEADER] = invokedBy;
|
|
70
|
+
}
|
|
71
|
+
const isBinary = Buffer.isBuffer(request.body);
|
|
72
|
+
if (request.body !== undefined && !headers["content-type"]) {
|
|
73
|
+
headers["content-type"] = isBinary ? "application/octet-stream" : "application/json";
|
|
74
|
+
}
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
return {
|
|
77
|
+
version: "2.0",
|
|
78
|
+
routeKey: "$default",
|
|
79
|
+
rawPath: request.path,
|
|
80
|
+
rawQueryString: new URLSearchParams(request.query ?? {}).toString(),
|
|
81
|
+
headers,
|
|
82
|
+
...(request.cookies?.length ? { cookies: request.cookies } : {}),
|
|
83
|
+
requestContext: {
|
|
84
|
+
accountId: "",
|
|
85
|
+
apiId: options.invoke ? "lambder-invoke" : "lambder-local",
|
|
86
|
+
domainName: request.host,
|
|
87
|
+
domainPrefix: "",
|
|
88
|
+
http: {
|
|
89
|
+
method: request.method,
|
|
90
|
+
path: request.path,
|
|
91
|
+
protocol: "HTTP/1.1",
|
|
92
|
+
sourceIp: request.clientIp ?? "",
|
|
93
|
+
userAgent: options.invoke ? "lambder-invoke" : "lambder-local",
|
|
94
|
+
},
|
|
95
|
+
requestId: randomRequestId(),
|
|
96
|
+
routeKey: "$default",
|
|
97
|
+
stage: "$default",
|
|
98
|
+
time: new Date(now).toISOString(),
|
|
99
|
+
timeEpoch: now,
|
|
100
|
+
},
|
|
101
|
+
...(request.body !== undefined
|
|
102
|
+
? { body: isBinary ? bytesToBase64(request.body) : request.body }
|
|
103
|
+
: {}),
|
|
104
|
+
isBase64Encoded: isBinary,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The body envelope LambderCaller sends, minus the fields only a browser has
|
|
109
|
+
* a value for, as JSON. A plain payload arrives already serialized (the
|
|
110
|
+
* compression decision needed its JSON) and is spliced in rather than
|
|
111
|
+
* parsed and stringified a second time; a compressed one rides as its two
|
|
112
|
+
* fields.
|
|
113
|
+
*/
|
|
114
|
+
export const buildEnvelopeJson = (fields) => {
|
|
115
|
+
// The field set is buildEnvelopeFields', so a field added to the envelope
|
|
116
|
+
// reaches this sender too; only the payload is this one's own business.
|
|
117
|
+
const withoutPayload = JSON.stringify(buildEnvelopeFields({
|
|
118
|
+
apiName: fields.apiName,
|
|
119
|
+
version: fields.version,
|
|
120
|
+
token: fields.csrf ?? "",
|
|
121
|
+
siteHost: fields.siteHost,
|
|
122
|
+
compressed: fields.compressed,
|
|
123
|
+
guardInputs: fields.guardInputs,
|
|
124
|
+
idempotencyKey: fields.idempotencyKey,
|
|
125
|
+
}));
|
|
126
|
+
if (fields.payloadJson === undefined)
|
|
127
|
+
return withoutPayload;
|
|
128
|
+
return `${withoutPayload.slice(0, -1)},"payload":${fields.payloadJson}}`;
|
|
129
|
+
};
|
|
130
|
+
/** The function's answer as an HTTP result; throws when it is not one, or its compressed body cannot be restored under `maxBodyBytes`. */
|
|
131
|
+
export const decodeLambdaHttpResult = async (result, maxBodyBytes) => {
|
|
132
|
+
if (!result || typeof result !== "object" || typeof result.statusCode !== "number") {
|
|
133
|
+
throw new Error("the function did not answer with an HTTP response object; is it a Lambder app?");
|
|
134
|
+
}
|
|
135
|
+
const raw = result;
|
|
136
|
+
const headers = {};
|
|
137
|
+
const cookies = [...(raw.cookies ?? [])];
|
|
138
|
+
for (const [key, value] of Object.entries(raw.headers ?? {}))
|
|
139
|
+
headers[key.toLowerCase()] = value;
|
|
140
|
+
for (const [key, values] of Object.entries(raw.multiValueHeaders ?? {})) {
|
|
141
|
+
// A v1 answer carries Set-Cookie as a multi-value header; keep the
|
|
142
|
+
// values apart, as the v2 cookies array does, since they contain commas.
|
|
143
|
+
if (key.toLowerCase() === "set-cookie")
|
|
144
|
+
cookies.push(...values);
|
|
145
|
+
headers[key.toLowerCase()] = values.join(", ");
|
|
146
|
+
}
|
|
147
|
+
let body = raw.body ? Buffer.from(raw.body, raw.isBase64Encoded ? "base64" : "utf8") : Buffer.alloc(0);
|
|
148
|
+
const encoding = headers["content-encoding"]?.trim().toLowerCase();
|
|
149
|
+
if (encoding === "br" || encoding === "gzip") {
|
|
150
|
+
// restoreBytes, not restoreText: a route may answer compressed
|
|
151
|
+
// binary (a wasm module, anything it forced compression on), and
|
|
152
|
+
// decoding that as UTF-8 first would replace every byte that is
|
|
153
|
+
// not valid UTF-8 and hand back a silently different body.
|
|
154
|
+
const restored = await restoreBytes(body, encoding, { maxBytes: maxBodyBytes });
|
|
155
|
+
body = Buffer.isBuffer(restored) ? restored : Buffer.from(restored);
|
|
156
|
+
}
|
|
157
|
+
else if (encoding && encoding !== "identity") {
|
|
158
|
+
// "identity" is a legal value meaning no encoding, and a hook or a
|
|
159
|
+
// proxy may set it; treating it as unsupported turned every such
|
|
160
|
+
// invoke into a protocol failure.
|
|
161
|
+
throw new Error(`the answer carries an unsupported Content-Encoding "${encoding}"`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
statusCode: raw.statusCode,
|
|
165
|
+
headers,
|
|
166
|
+
cookies,
|
|
167
|
+
body,
|
|
168
|
+
text: () => body.toString("utf8"),
|
|
169
|
+
json: () => JSON.parse(body.toString("utf8")),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
/** A Lambda context for an in-process call, the fields a handler might read filled plausibly. */
|
|
173
|
+
export const localLambdaContext = (functionName, overrides = {}) => ({
|
|
174
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
175
|
+
functionName,
|
|
176
|
+
functionVersion: "$LATEST",
|
|
177
|
+
invokedFunctionArn: `arn:aws:lambda:local:000000000000:function:${functionName}`,
|
|
178
|
+
memoryLimitInMB: "128",
|
|
179
|
+
awsRequestId: randomRequestId(),
|
|
180
|
+
logGroupName: `/aws/lambda/${functionName}`,
|
|
181
|
+
logStreamName: "local",
|
|
182
|
+
getRemainingTimeInMillis: () => 30_000,
|
|
183
|
+
done: () => { },
|
|
184
|
+
fail: () => { },
|
|
185
|
+
succeed: () => { },
|
|
186
|
+
...overrides,
|
|
187
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Context } from "aws-lambda";
|
|
2
|
+
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
3
|
+
import type { LambderHandler } from "../core/LambderCreateOptions.js";
|
|
4
|
+
export type LambderHandlerTransportOptions = {
|
|
5
|
+
/** The Host the handler sees (ctx.host), and the siteHost the envelope carries when the caller has none. Default: the apiPath's own host when it is absolute, otherwise "localhost". */
|
|
6
|
+
host?: string;
|
|
7
|
+
/** The client IP the handler sees (ctx.ip). Default: "127.0.0.1". */
|
|
8
|
+
clientIp?: string;
|
|
9
|
+
/** Ceiling on what a compressed answer may restore to. Default: 20,000,000. */
|
|
10
|
+
maxResponseBytes?: number;
|
|
11
|
+
/** Fields of the Lambda context the handler receives. */
|
|
12
|
+
context?: Partial<Context>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A transport that calls a Lambder handler in this process, the way a
|
|
16
|
+
* browser's request would reach it: a browser-shaped API Gateway event (no
|
|
17
|
+
* invoke marker), the real handler, and its answer decoded back, compressed
|
|
18
|
+
* bodies included. With the memory stores, that is an integration test of a
|
|
19
|
+
* real app through the typed caller with no HTTP and no AWS. Wrap it in
|
|
20
|
+
* lambderCookieJarTransport to hold a session across calls.
|
|
21
|
+
*
|
|
22
|
+
* A handler that throws (which a Lambder app never does on the HTTP path,
|
|
23
|
+
* since render() answers its own last-resort 500) produced no answer at all,
|
|
24
|
+
* so the call fails as `protocol` carrying the handler's own error as its
|
|
25
|
+
* cause. API Gateway would have turned it into a bare 502, and synthesizing
|
|
26
|
+
* one here would throw the error away; keeping it is the point of an
|
|
27
|
+
* in-process transport, and there is nowhere in an HTTP answer to put one
|
|
28
|
+
* except the user-facing `message` field, which is the wrong channel for an
|
|
29
|
+
* internal fault.
|
|
30
|
+
*
|
|
31
|
+
* `request.signal` ends the wait, as the transport contract requires. The
|
|
32
|
+
* handler keeps running to completion either way, because a function call in
|
|
33
|
+
* this process cannot be cancelled: what a timeout buys here is the caller's
|
|
34
|
+
* answer, not the callee's attention.
|
|
35
|
+
*/
|
|
36
|
+
export declare const lambderHandlerTransport: (handler: LambderHandler, options?: LambderHandlerTransportOptions) => LambderApiTransport;
|