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
|
@@ -10,17 +10,23 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { restoreBytes } from "../shared/wire/LambderCompressionCodec.js";
|
|
12
12
|
import { bytesToBase64 } from "../shared/util/LambderBase64.js";
|
|
13
|
+
import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
|
|
13
14
|
import { buildEnvelopeFields } from "../shared/transport/LambderApiTransport.js";
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Marks a synthesized request as an invoke, for guards and hooks that want to
|
|
17
|
+
* tell. Not an authorization: over HTTP it is a header any client can send.
|
|
18
|
+
* The server itself tells an invoke by its requestContext.apiId, which no
|
|
19
|
+
* gateway lets a client write (see LAMBDER_INVOKE_API_ID).
|
|
20
|
+
*/
|
|
15
21
|
export const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
|
|
16
22
|
/** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
|
|
17
23
|
export const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
|
|
18
24
|
/** The value of the marker header; a future incompatible event shape would bump it. */
|
|
19
25
|
export const LAMBDER_INVOKE_PROTOCOL = "1";
|
|
20
26
|
/**
|
|
21
|
-
* The forwarded-address header a gateway writes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
27
|
+
* The forwarded-address header a gateway writes, which an invoke never
|
|
28
|
+
* carries: its address travels as `clientIp` alone (see
|
|
29
|
+
* synthesizeLambdaHttpEvent).
|
|
24
30
|
*/
|
|
25
31
|
const FORWARDED_FOR_HEADER = "x-forwarded-for";
|
|
26
32
|
/** The session's cookie pair for a synthesized event: the token rides as a cookie, the CSRF value in the envelope's `token` field. */
|
|
@@ -31,35 +37,21 @@ const randomRequestId = () => {
|
|
|
31
37
|
return webCrypto.randomUUID();
|
|
32
38
|
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
33
39
|
};
|
|
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) => {
|
|
40
|
+
export function synthesizeLambdaHttpEvent(request, options) {
|
|
46
41
|
// The caller's own headers go on first, so the ones this function owns
|
|
47
|
-
// cannot be displaced by them.
|
|
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.
|
|
42
|
+
// cannot be displaced by them.
|
|
51
43
|
const headers = {};
|
|
52
44
|
for (const [key, value] of Object.entries(request.headers ?? {}))
|
|
53
45
|
headers[key.toLowerCase()] = value;
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// ctx.ip, and let a browser-shaped request claim to be an invoke.
|
|
60
|
-
delete headers[FORWARDED_FOR_HEADER];
|
|
46
|
+
// The invoke markers the event owns whatever the caller passed: they say
|
|
47
|
+
// what this event is. Forwarding a browser's headers wholesale is an
|
|
48
|
+
// ordinary gateway-lambda pattern, and without these deletes it would let
|
|
49
|
+
// a browser-shaped request claim to be an invoke, or an invoke name an
|
|
50
|
+
// invoking function that did not send it.
|
|
61
51
|
delete headers[LAMBDER_INVOKE_HEADER];
|
|
62
52
|
delete headers[LAMBDER_INVOKED_BY_HEADER];
|
|
53
|
+
if (options.invoke)
|
|
54
|
+
delete headers[FORWARDED_FOR_HEADER];
|
|
63
55
|
headers.host = request.host;
|
|
64
56
|
headers["accept-encoding"] = "br, gzip";
|
|
65
57
|
if (options.invoke) {
|
|
@@ -69,25 +61,78 @@ export const synthesizeLambdaHttpEvent = (request, options) => {
|
|
|
69
61
|
headers[LAMBDER_INVOKED_BY_HEADER] = invokedBy;
|
|
70
62
|
}
|
|
71
63
|
const isBinary = Buffer.isBuffer(request.body);
|
|
72
|
-
if (request.
|
|
64
|
+
if (request.contentType)
|
|
65
|
+
headers["content-type"] = request.contentType;
|
|
66
|
+
else if (request.body !== undefined && !headers["content-type"]) {
|
|
73
67
|
headers["content-type"] = isBinary ? "application/octet-stream" : "application/json";
|
|
74
68
|
}
|
|
69
|
+
const body = request.body === undefined ? undefined : isBinary ? bytesToBase64(request.body) : request.body;
|
|
75
70
|
const now = Date.now();
|
|
71
|
+
if (options.eventFormat === "v1") {
|
|
72
|
+
// A REST API has no cookies array: cookies ride in the Cookie header,
|
|
73
|
+
// and every header is delivered twice, once as its last value and
|
|
74
|
+
// once as the list of all of them.
|
|
75
|
+
if (request.cookies?.length)
|
|
76
|
+
headers.cookie = request.cookies.join("; ");
|
|
77
|
+
const query = request.query && Object.keys(request.query).length ? request.query : null;
|
|
78
|
+
return {
|
|
79
|
+
resource: "/{proxy+}",
|
|
80
|
+
path: request.path,
|
|
81
|
+
httpMethod: request.method,
|
|
82
|
+
headers,
|
|
83
|
+
multiValueHeaders: Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, [value]])),
|
|
84
|
+
queryStringParameters: query,
|
|
85
|
+
multiValueQueryStringParameters: query ? Object.fromEntries(Object.entries(query).map(([name, value]) => [name, [value]])) : null,
|
|
86
|
+
pathParameters: null,
|
|
87
|
+
stageVariables: null,
|
|
88
|
+
// The fields a handler might read, filled plausibly; the rest of
|
|
89
|
+
// a REST API's request context (authorizer, the API key, the
|
|
90
|
+
// Cognito identity) describes a deployment this event has none of.
|
|
91
|
+
requestContext: {
|
|
92
|
+
accountId: "",
|
|
93
|
+
apiId: options.invoke ? LAMBDER_INVOKE_API_ID : LAMBDER_LOCAL_API_ID,
|
|
94
|
+
domainName: request.host,
|
|
95
|
+
httpMethod: request.method,
|
|
96
|
+
identity: { sourceIp: request.clientIp ?? "", userAgent: "lambder-local" },
|
|
97
|
+
path: request.path,
|
|
98
|
+
protocol: "HTTP/1.1",
|
|
99
|
+
requestId: randomRequestId(),
|
|
100
|
+
requestTimeEpoch: now,
|
|
101
|
+
resourcePath: "/{proxy+}",
|
|
102
|
+
stage: "local",
|
|
103
|
+
},
|
|
104
|
+
body: body ?? null,
|
|
105
|
+
isBase64Encoded: isBinary,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// An HTTP API's event, with its path decoded (an encoded slash into a
|
|
109
|
+
// separator, too) the way the gateway delivers it, whatever host the
|
|
110
|
+
// request names. The server reads a gateway's v2 event as encoded when
|
|
111
|
+
// its domain is a Function URL's, so it is told this event is decoded by
|
|
112
|
+
// the apiId, which is Lambder's own either way: a request naming a
|
|
113
|
+
// lambda-url host would otherwise have its path decoded a second time,
|
|
114
|
+
// and `/%2561dmin` would reach `/admin`. A path that does not decode goes
|
|
115
|
+
// as it is.
|
|
116
|
+
let decodedPath = request.path;
|
|
117
|
+
try {
|
|
118
|
+
decodedPath = decodeURIComponent(request.path);
|
|
119
|
+
}
|
|
120
|
+
catch { /* delivered as written */ }
|
|
76
121
|
return {
|
|
77
122
|
version: "2.0",
|
|
78
123
|
routeKey: "$default",
|
|
79
|
-
rawPath:
|
|
124
|
+
rawPath: decodedPath,
|
|
80
125
|
rawQueryString: new URLSearchParams(request.query ?? {}).toString(),
|
|
81
126
|
headers,
|
|
82
127
|
...(request.cookies?.length ? { cookies: request.cookies } : {}),
|
|
83
128
|
requestContext: {
|
|
84
129
|
accountId: "",
|
|
85
|
-
apiId: options.invoke ?
|
|
130
|
+
apiId: options.invoke ? LAMBDER_INVOKE_API_ID : LAMBDER_LOCAL_API_ID,
|
|
86
131
|
domainName: request.host,
|
|
87
132
|
domainPrefix: "",
|
|
88
133
|
http: {
|
|
89
134
|
method: request.method,
|
|
90
|
-
path:
|
|
135
|
+
path: decodedPath,
|
|
91
136
|
protocol: "HTTP/1.1",
|
|
92
137
|
sourceIp: request.clientIp ?? "",
|
|
93
138
|
userAgent: options.invoke ? "lambder-invoke" : "lambder-local",
|
|
@@ -98,12 +143,10 @@ export const synthesizeLambdaHttpEvent = (request, options) => {
|
|
|
98
143
|
time: new Date(now).toISOString(),
|
|
99
144
|
timeEpoch: now,
|
|
100
145
|
},
|
|
101
|
-
...(
|
|
102
|
-
? { body: isBinary ? bytesToBase64(request.body) : request.body }
|
|
103
|
-
: {}),
|
|
146
|
+
...(body !== undefined ? { body } : {}),
|
|
104
147
|
isBase64Encoded: isBinary,
|
|
105
148
|
};
|
|
106
|
-
}
|
|
149
|
+
}
|
|
107
150
|
/**
|
|
108
151
|
* The body envelope LambderCaller sends, minus the fields only a browser has
|
|
109
152
|
* a value for, as JSON. A plain payload arrives already serialized (the
|
|
@@ -157,8 +200,8 @@ export const decodeLambdaHttpResult = async (result, maxBodyBytes) => {
|
|
|
157
200
|
}
|
|
158
201
|
else if (encoding && encoding !== "identity") {
|
|
159
202
|
// "identity" is a legal value meaning no encoding, and a hook or a
|
|
160
|
-
// proxy may set it
|
|
161
|
-
//
|
|
203
|
+
// proxy may set it, so it passes as a plain body; only other values
|
|
204
|
+
// fail the invoke.
|
|
162
205
|
throw new Error(`the answer carries an unsupported Content-Encoding "${encoding}"`);
|
|
163
206
|
}
|
|
164
207
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from "aws-lambda";
|
|
2
2
|
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
3
3
|
import type { LambderHandler } from "../core/LambderCreateOptions.js";
|
|
4
|
+
import type { LambderHttpEventFormat } from "../core/LambderContext.js";
|
|
4
5
|
export type LambderHandlerTransportOptions = {
|
|
5
6
|
/** 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
7
|
host?: string;
|
|
@@ -10,6 +11,8 @@ export type LambderHandlerTransportOptions = {
|
|
|
10
11
|
maxResponseBytes?: number;
|
|
11
12
|
/** Fields of the Lambda context the handler receives. */
|
|
12
13
|
context?: Partial<Context>;
|
|
14
|
+
/** The gateway shape the handler is called with: "v2" (an HTTP API, a Function URL) or "v1" (a REST API). Default: "v2". A handler answers both alike; name the one your deployment delivers when the difference is what you are testing. */
|
|
15
|
+
eventFormat?: LambderHttpEventFormat;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
18
|
* A transport that calls a Lambder handler in this process, the way a
|
|
@@ -20,17 +23,16 @@ export type LambderHandlerTransportOptions = {
|
|
|
20
23
|
* lambderCookieJarTransport to hold a session across calls.
|
|
21
24
|
*
|
|
22
25
|
* 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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* internal fault.
|
|
26
|
+
* since render() answers its own last-resort 500) produced no answer, so the
|
|
27
|
+
* call fails as `protocol` with the handler's own error as its cause. API
|
|
28
|
+
* Gateway would turn it into a bare 502, but synthesizing one here would
|
|
29
|
+
* throw the error away, and keeping it is the point of an in-process
|
|
30
|
+
* transport. An HTTP answer's only place for it would be the user-facing
|
|
31
|
+
* `message` field, the wrong channel for an internal fault.
|
|
30
32
|
*
|
|
31
33
|
* `request.signal` ends the wait, as the transport contract requires. The
|
|
32
|
-
* handler
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* handler still runs to completion, because a function call in this process
|
|
35
|
+
* cannot be cancelled: a timeout buys the caller its answer, not the
|
|
36
|
+
* callee's attention.
|
|
35
37
|
*/
|
|
36
38
|
export declare const lambderHandlerTransport: (handler: LambderHandler, options?: LambderHandlerTransportOptions) => LambderApiTransport;
|
|
@@ -15,18 +15,17 @@ const isAbortError = (err, signal) => signal !== undefined && signal.aborted &&
|
|
|
15
15
|
* lambderCookieJarTransport to hold a session across calls.
|
|
16
16
|
*
|
|
17
17
|
* A handler that throws (which a Lambder app never does on the HTTP path,
|
|
18
|
-
* since render() answers its own last-resort 500) produced no answer
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* internal fault.
|
|
18
|
+
* since render() answers its own last-resort 500) produced no answer, so the
|
|
19
|
+
* call fails as `protocol` with the handler's own error as its cause. API
|
|
20
|
+
* Gateway would turn it into a bare 502, but synthesizing one here would
|
|
21
|
+
* throw the error away, and keeping it is the point of an in-process
|
|
22
|
+
* transport. An HTTP answer's only place for it would be the user-facing
|
|
23
|
+
* `message` field, the wrong channel for an internal fault.
|
|
25
24
|
*
|
|
26
25
|
* `request.signal` ends the wait, as the transport contract requires. The
|
|
27
|
-
* handler
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* handler still runs to completion, because a function call in this process
|
|
27
|
+
* cannot be cancelled: a timeout buys the caller its answer, not the
|
|
28
|
+
* callee's attention.
|
|
30
29
|
*/
|
|
31
30
|
export const lambderHandlerTransport = (handler, options = {}) => {
|
|
32
31
|
const clientIp = options.clientIp ?? LOOPBACK_CLIENT_IP;
|
|
@@ -47,10 +46,11 @@ export const lambderHandlerTransport = (handler, options = {}) => {
|
|
|
47
46
|
path: target.path,
|
|
48
47
|
host,
|
|
49
48
|
headers: request.headers,
|
|
49
|
+
contentType: "application/json",
|
|
50
50
|
clientIp: request.clientIp ?? clientIp,
|
|
51
51
|
cookies: request.cookies,
|
|
52
52
|
body: JSON.stringify(buildTransportEnvelope({ ...request, siteHost: request.siteHost || host })),
|
|
53
|
-
}, { invoke: false });
|
|
53
|
+
}, { invoke: false, eventFormat: options.eventFormat });
|
|
54
54
|
let result;
|
|
55
55
|
try {
|
|
56
56
|
result = await stopWaitingWhenAborted(handler(event, localLambdaContext("lambder-local", options.context)), request.signal);
|
|
@@ -58,18 +58,15 @@ export const lambderHandlerTransport = (handler, options = {}) => {
|
|
|
58
58
|
catch (err) {
|
|
59
59
|
if (isAbortError(err, request.signal))
|
|
60
60
|
throw err;
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// channel that carries a cause: swallowing it into a synthetic 502
|
|
65
|
-
// left the caller an outcome.error reading "Request failed: 502"
|
|
66
|
-
// and no way to reach what actually threw.
|
|
61
|
+
// A transport failure is the one channel that carries a cause. A
|
|
62
|
+
// synthetic 502 would leave the caller an outcome.error reading
|
|
63
|
+
// "Request failed: 502" and no way to reach what actually threw.
|
|
67
64
|
throw new LambderTransportFailure("protocol", `the handler threw instead of answering: ${coerceToError(err).message}`, { cause: err });
|
|
68
65
|
}
|
|
69
66
|
// Decoding failures are the callee answering with something that is
|
|
70
67
|
// not an HTTP result, or with more than the ceiling allows. Neither is
|
|
71
|
-
// a network failure, and reporting
|
|
72
|
-
// debugging an integration test
|
|
68
|
+
// a network failure, and reporting one as such would send whoever is
|
|
69
|
+
// debugging an integration test to look at their connection.
|
|
73
70
|
let http;
|
|
74
71
|
try {
|
|
75
72
|
http = await decodeLambdaHttpResult(result, maxResponseBytes);
|
|
@@ -13,13 +13,13 @@ import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockT
|
|
|
13
13
|
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockPublicNames, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSessionNames, LambderMockSlice, LambderMockOverride } from "./LambderMockTypes.js";
|
|
14
14
|
/**
|
|
15
15
|
* The mock runtime: the API core (LambderApiPipeline, the same class the
|
|
16
|
-
* Lambda server runs) over memory stores, with
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* Lambda server runs) over memory stores, with typed mock handlers and mock
|
|
17
|
+
* guards where the server has app handlers and guards. Every protocol step
|
|
18
|
+
* (envelope, refusals, sessions and their cookies, guards, rate limits,
|
|
19
|
+
* idempotency, the signature gate) happens in the core; this class resolves
|
|
20
|
+
* a name to an entry, wraps the handler's return into the envelope, and adds
|
|
21
|
+
* what a mock needs: failure injection, latency, a subscription, a call log,
|
|
22
|
+
* reset.
|
|
23
23
|
*
|
|
24
24
|
* Create one with initLambderMock<Contract, SessionData>().create(...),
|
|
25
25
|
* which fixes the contract and session types first so everything else is
|
|
@@ -34,12 +34,11 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
34
34
|
readonly tokenCookieKey: string;
|
|
35
35
|
readonly csrfCookieKey: string;
|
|
36
36
|
/**
|
|
37
|
-
* The client IP a transport request carrying none is read as. Public
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* clients where there was one.
|
|
37
|
+
* The client IP a transport request carrying none is read as. Public so
|
|
38
|
+
* every adapter reads the same default the direct transport uses: with a
|
|
39
|
+
* default of its own, an app that set defaultClientIp would show one
|
|
40
|
+
* address through the transport and another through the service worker,
|
|
41
|
+
* and a per-IP rate limit would count two clients where there is one.
|
|
43
42
|
*/
|
|
44
43
|
readonly defaultClientIp: string;
|
|
45
44
|
/** The host this runtime's cookies belong to (see the cookieHost option). */
|
|
@@ -47,9 +46,8 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
47
46
|
/**
|
|
48
47
|
* The API core, every protocol step of it. Private: the mock's surface is
|
|
49
48
|
* the app, and a consumer reaching past it would be configuring the
|
|
50
|
-
* server's pipeline through a development tool. The
|
|
51
|
-
*
|
|
52
|
-
* requestFromTransport), which is why none of them names this.
|
|
49
|
+
* server's pipeline through a development tool. The adapters use the
|
|
50
|
+
* app's own methods (handleRequest, requestFromTransport) instead.
|
|
53
51
|
*/
|
|
54
52
|
private readonly pipeline;
|
|
55
53
|
/** The cookie scope signIn plants under, so signOut can name the same one when it clears them. */
|
|
@@ -71,11 +69,10 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
71
69
|
* the ones the server runs on a definition, and the mock's own "a session
|
|
72
70
|
* endpoint needs the sessions option".
|
|
73
71
|
*
|
|
74
|
-
* One place, so no registration path can skip either check.
|
|
75
|
-
* endpoint on a mock without sessions would
|
|
76
|
-
* and answer
|
|
77
|
-
*
|
|
78
|
-
* create().
|
|
72
|
+
* One place, so no registration path can skip either check. Without the
|
|
73
|
+
* second, a session endpoint on a mock without sessions would register
|
|
74
|
+
* silently and answer its first call with a 500 naming the SERVER's
|
|
75
|
+
* option, for a mistake fixed by one option at create().
|
|
79
76
|
*/
|
|
80
77
|
private assertEntryRegistration;
|
|
81
78
|
private buildEntry;
|
|
@@ -87,14 +84,13 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
87
84
|
* A public endpoint deliberately left without a mock; a call answers the
|
|
88
85
|
* notMocked refusal carrying the reason.
|
|
89
86
|
*
|
|
90
|
-
* Public and session
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* is a type, so the builder is where it has to be said.
|
|
87
|
+
* Public and session get separate builders, as publicApi and sessionApi
|
|
88
|
+
* do, because the refusal runs through the pipeline so the steps BEFORE
|
|
89
|
+
* dispatch still happen, and the session read is one of them. Were every
|
|
90
|
+
* not-mocked endpoint public, a session endpoint with no session would
|
|
91
|
+
* answer "not mocked" where the server answers sessionExpired, and its
|
|
92
|
+
* events and call log would carry the wrong mode. The contract is a type,
|
|
93
|
+
* so the mode cannot be recovered at runtime: the builder has to say it.
|
|
98
94
|
*/
|
|
99
95
|
notMocked<K extends LambderMockPublicNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
|
|
100
96
|
/** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
|
|
@@ -107,23 +103,21 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
107
103
|
* mockApp.register(userMocks, billingMocks, mockApp.restNotMocked("not mocked yet"));
|
|
108
104
|
* ```
|
|
109
105
|
*
|
|
110
|
-
*
|
|
111
|
-
* register() stays exhaustive by construction, and
|
|
112
|
-
* claims answer the notMocked refusal
|
|
106
|
+
* It lets mocks be adopted over a contract they do not cover yet:
|
|
107
|
+
* register() stays exhaustive by construction, and endpoints nothing
|
|
108
|
+
* claims answer the notMocked refusal with this reason instead of
|
|
113
109
|
* apiNotFound, so a screen that reaches one says "not mocked yet" rather
|
|
114
110
|
* than "unknown error". Strays and duplicates in the explicit slices are
|
|
115
|
-
* refused
|
|
116
|
-
*
|
|
117
|
-
* rest.
|
|
111
|
+
* still refused, and an entry registered later (registerPartial, or a
|
|
112
|
+
* second register) takes its endpoint back from the rest.
|
|
118
113
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* sessionNotMocked instead.
|
|
114
|
+
* What it cannot do is the session read. The mode of an unregistered name
|
|
115
|
+
* is not knowable at runtime (the contract is a type), so a call it
|
|
116
|
+
* answers is processed as public: the protocol's pre-pass still runs, so
|
|
117
|
+
* a stale client still hears versionExpired, but a signed-out call to an
|
|
118
|
+
* unmocked session endpoint answers "not mocked" where the server answers
|
|
119
|
+
* sessionExpired. Declare an endpoint whose signed-out path a test cares
|
|
120
|
+
* about with sessionNotMocked instead.
|
|
127
121
|
*/
|
|
128
122
|
restNotMocked(reason: string): LambderMockRestEntry;
|
|
129
123
|
private buildNotMockedEntry;
|
|
@@ -161,24 +155,20 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
161
155
|
* Whether a call to this name would be answered from the registry, which
|
|
162
156
|
* is what an adapter asks before passing one on.
|
|
163
157
|
*
|
|
164
|
-
* True for every name once a rest entry is registered,
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* answers everything itself and nothing is handed on to the network.
|
|
158
|
+
* True for every name once a rest entry is registered, since it answers
|
|
159
|
+
* whatever nothing else claimed. That makes a rest entry and the MSW
|
|
160
|
+
* adapter's `onUnmocked: "passthrough"` alternatives rather than layers:
|
|
161
|
+
* with one registered, nothing is handed on to the network.
|
|
169
162
|
*/
|
|
170
163
|
hasRegisteredEntry(apiName: string): boolean;
|
|
171
164
|
private entryFor;
|
|
172
165
|
/**
|
|
173
166
|
* The entry that answers a name nothing registered, when register() was
|
|
174
167
|
* given a rest entry: the notMocked refusal carrying its reason, run
|
|
175
|
-
* through the pipeline as a public endpoint
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* still runs (the signature gate, the payload restore); the session read is
|
|
180
|
-
* the one step this answer cannot have, which is the fidelity limit
|
|
181
|
-
* restNotMocked documents.
|
|
168
|
+
* through the pipeline as a public endpoint, since the mode of an
|
|
169
|
+
* unregistered name cannot be recovered at runtime. Everything before
|
|
170
|
+
* dispatch still runs (the signature gate, the payload restore); the
|
|
171
|
+
* missing session read is the fidelity limit restNotMocked documents.
|
|
182
172
|
*/
|
|
183
173
|
private restNotMockedEntry;
|
|
184
174
|
/** The next call to the endpoint fails this way; several calls queue in order. */
|
|
@@ -194,30 +184,24 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
194
184
|
* latency, the call log and its numbering, the cookies its own transports
|
|
195
185
|
* hold, then onReset, so the app rewinds its own data too.
|
|
196
186
|
*
|
|
197
|
-
* The cookies matter as much as the sessions
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* for itself is emptied, and the cookies a "document" transport mirrored
|
|
202
|
-
* are expired again.
|
|
187
|
+
* The cookies matter as much as the sessions: a jar still holding the
|
|
188
|
+
* token of an emptied store's session reads as signed in until an answer
|
|
189
|
+
* says sessionExpired. So every jar transport() built for itself is
|
|
190
|
+
* emptied, and the cookies a "document" transport mirrored are expired.
|
|
203
191
|
*
|
|
204
|
-
* The registry survives, being
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* does not know what else holds it.
|
|
192
|
+
* The registry survives, being configuration rather than accumulated
|
|
193
|
+
* state. Subscriptions survive too, being how a test watches the runtime;
|
|
194
|
+
* a listener muted for throwing is unmuted, so one bad call does not
|
|
195
|
+
* silence it for the rest of the run. A session store or cookie jar the
|
|
196
|
+
* app supplied survives: the runtime did not create it and does not know
|
|
197
|
+
* what else holds it.
|
|
211
198
|
*/
|
|
212
199
|
reset(): void;
|
|
213
200
|
/**
|
|
214
201
|
* The four session members refuse in the mock's own words, naming the
|
|
215
|
-
* option a mock is created with.
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* which is the SERVER's option name: the mock's is `sessions`, and a
|
|
219
|
-
* reader who goes looking for `session` on create() does not find it.
|
|
220
|
-
* The registration path was fixed for exactly this one method over.
|
|
202
|
+
* option a mock is created with. The pipeline's guard names the SERVER's
|
|
203
|
+
* option (`session`), which a reader does not find on the mock's create()
|
|
204
|
+
* (`sessions`); assertEntryRegistration does the same for registration.
|
|
221
205
|
*/
|
|
222
206
|
private assertSessionsConfigured;
|
|
223
207
|
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when sessions are off. */
|
|
@@ -301,9 +285,9 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
301
285
|
/**
|
|
302
286
|
* Takes a jar an adapter built for itself as the runtime's own, so reset()
|
|
303
287
|
* empties it with the rest. The MSW adapter's jar holds the session
|
|
304
|
-
* cookies of calls that never touch transport()
|
|
305
|
-
*
|
|
306
|
-
*
|
|
288
|
+
* cookies of calls that never touch transport(); left full after a reset,
|
|
289
|
+
* the next request would carry a token for a session the emptied store no
|
|
290
|
+
* longer has.
|
|
307
291
|
*/
|
|
308
292
|
adoptCookieJar(jar: LambderCookieJar): void;
|
|
309
293
|
/** caller.setTransport(mockApp.transport(options)); returns the transport, its jar on it. */
|
|
@@ -341,12 +325,12 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
|
|
|
341
325
|
* The mock app, with the guard map and the rate-limit policies inferred
|
|
342
326
|
* from the options.
|
|
343
327
|
*
|
|
344
|
-
* `const` on each of them
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
328
|
+
* `const` on each of them pins a restatement to the contract, at a cost:
|
|
329
|
+
* inferring a generic from an object literal switches excess-property
|
|
330
|
+
* checking off for the whole literal, nested objects included, so a typo
|
|
331
|
+
* inside `rateLimits.policies` or `idempotency` would compile and be
|
|
332
|
+
* dropped. `I` exists for the same reason `P` does, and
|
|
333
|
+
* LambderMockSurplusKeys puts the error back on the key.
|
|
350
334
|
*/
|
|
351
335
|
create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>>(options: LambderMockAppOptions<C, S, G, P, I>): LambderMockApp<C, S, G>;
|
|
352
336
|
};
|