lambder 6.0.1 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +26 -24
- 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,89 @@
|
|
|
1
|
+
import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
|
|
2
|
+
import { buildTransportEnvelope, LambderTransportFailure, resolveApiPathTarget } from "../shared/transport/LambderApiTransport.js";
|
|
3
|
+
import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderRequestPayload.js";
|
|
4
|
+
import { stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
|
|
5
|
+
import { LOOPBACK_CLIENT_IP } from "../shared/util/LambderClientIp.js";
|
|
6
|
+
import { decodeLambdaHttpResult, localLambdaContext, synthesizeLambdaHttpEvent } from "./LambderLambdaEvent.js";
|
|
7
|
+
/** The rejection an abort produces here: the signal's own reason, which is a DOMException("AbortError") unless the aborting code named another. */
|
|
8
|
+
const isAbortError = (err, signal) => signal !== undefined && signal.aborted && err === signal.reason;
|
|
9
|
+
/**
|
|
10
|
+
* A transport that calls a Lambder handler in this process, the way a
|
|
11
|
+
* browser's request would reach it: a browser-shaped API Gateway event (no
|
|
12
|
+
* invoke marker), the real handler, and its answer decoded back, compressed
|
|
13
|
+
* bodies included. With the memory stores, that is an integration test of a
|
|
14
|
+
* real app through the typed caller with no HTTP and no AWS. Wrap it in
|
|
15
|
+
* lambderCookieJarTransport to hold a session across calls.
|
|
16
|
+
*
|
|
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 at all,
|
|
19
|
+
* so the call fails as `protocol` carrying the handler's own error as its
|
|
20
|
+
* cause. API Gateway would have turned it into a bare 502, and synthesizing
|
|
21
|
+
* one here would throw the error away; keeping it is the point of an
|
|
22
|
+
* in-process transport, and there is nowhere in an HTTP answer to put one
|
|
23
|
+
* except the user-facing `message` field, which is the wrong channel for an
|
|
24
|
+
* internal fault.
|
|
25
|
+
*
|
|
26
|
+
* `request.signal` ends the wait, as the transport contract requires. The
|
|
27
|
+
* handler keeps running to completion either way, because a function call in
|
|
28
|
+
* this process cannot be cancelled: what a timeout buys here is the caller's
|
|
29
|
+
* answer, not the callee's attention.
|
|
30
|
+
*/
|
|
31
|
+
export const lambderHandlerTransport = (handler, options = {}) => {
|
|
32
|
+
const clientIp = options.clientIp ?? LOOPBACK_CLIENT_IP;
|
|
33
|
+
const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES;
|
|
34
|
+
return async (request) => {
|
|
35
|
+
// Before anything is built or called: a call the caller has already
|
|
36
|
+
// given up on should not reach the handler at all, and an abort must
|
|
37
|
+
// travel as the signal's own reason rather than as a transport fault.
|
|
38
|
+
request.signal?.throwIfAborted();
|
|
39
|
+
// An absolute apiPath (what lambderFetchTransport tells a caller
|
|
40
|
+
// outside a browser to configure) is a URL, and a URL as the event's
|
|
41
|
+
// rawPath matches no route: every call would 404 on an app that is
|
|
42
|
+
// wired correctly.
|
|
43
|
+
const target = resolveApiPathTarget(request.apiPath);
|
|
44
|
+
const host = options.host ?? target.host ?? "localhost";
|
|
45
|
+
const event = synthesizeLambdaHttpEvent({
|
|
46
|
+
method: "POST",
|
|
47
|
+
path: target.path,
|
|
48
|
+
host,
|
|
49
|
+
headers: request.headers,
|
|
50
|
+
clientIp: request.clientIp ?? clientIp,
|
|
51
|
+
cookies: request.cookies,
|
|
52
|
+
body: JSON.stringify(buildTransportEnvelope({ ...request, siteHost: request.siteHost || host })),
|
|
53
|
+
}, { invoke: false });
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await stopWaitingWhenAborted(handler(event, localLambdaContext("lambder-local", options.context)), request.signal);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isAbortError(err, request.signal))
|
|
60
|
+
throw err;
|
|
61
|
+
// The whole point of this transport is in-process integration
|
|
62
|
+
// testing, so the handler's own error is the useful part. A thrown
|
|
63
|
+
// handler answered nothing, and a transport failure is the one
|
|
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.
|
|
67
|
+
throw new LambderTransportFailure("protocol", `the handler threw instead of answering: ${coerceToError(err).message}`, { cause: err });
|
|
68
|
+
}
|
|
69
|
+
// Decoding failures are the callee answering with something that is
|
|
70
|
+
// not an HTTP result, or with more than the ceiling allows. Neither is
|
|
71
|
+
// a network failure, and reporting them as one sends whoever is
|
|
72
|
+
// debugging an integration test looking at their connection.
|
|
73
|
+
let http;
|
|
74
|
+
try {
|
|
75
|
+
http = await decodeLambdaHttpResult(result, maxResponseBytes);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new LambderTransportFailure("protocol", coerceToError(err).message, { cause: err });
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: http.statusCode,
|
|
82
|
+
statusText: "",
|
|
83
|
+
header: (name) => http.headers[name.toLowerCase()] ?? null,
|
|
84
|
+
json: async () => http.json(),
|
|
85
|
+
text: async () => http.text(),
|
|
86
|
+
setCookies: http.cookies,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
3
|
+
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
4
|
+
import { type LambderApiAnswer } from "../api/LambderApiAnswer.js";
|
|
5
|
+
import { type LambderApiTransport, type LambderApiTransportRequest } from "../shared/transport/LambderApiTransport.js";
|
|
6
|
+
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
7
|
+
import { type LambderApiGuard, type LambderGuardBuilder } from "../api/LambderApiGuards.js";
|
|
8
|
+
import { LambderMemoryRateLimiter } from "../stores/LambderMemoryRateLimiter.js";
|
|
9
|
+
import { LambderMemoryIdempotencyStore } from "../stores/LambderMemoryIdempotencyStore.js";
|
|
10
|
+
import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.js";
|
|
11
|
+
import LambderSessionManager, { type LambderCreatedSession } from "../session/LambderSessionManager.js";
|
|
12
|
+
import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./LambderMockCreateOptions.js";
|
|
13
|
+
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockPublicNames, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSessionNames, LambderMockSlice, LambderMockOverride } from "./LambderMockTypes.js";
|
|
14
|
+
/**
|
|
15
|
+
* The mock runtime: the API core (LambderApiPipeline, the same class the
|
|
16
|
+
* Lambda server runs) over memory stores, with a registry of typed mock
|
|
17
|
+
* handlers where the server has app handlers, and mock guards where it has
|
|
18
|
+
* app guards. Everything the protocol does (envelope, refusals, sessions
|
|
19
|
+
* and their cookies, guards, rate limits, idempotency, the version gate)
|
|
20
|
+
* happens in the core; this class only resolves a name to an entry, wraps
|
|
21
|
+
* the handler's return into the envelope, and adds what a mock needs on
|
|
22
|
+
* top: failure injection, latency, a subscription, a call log, reset.
|
|
23
|
+
*
|
|
24
|
+
* Create one with initLambderMock<Contract, SessionData>().create(...),
|
|
25
|
+
* which fixes the contract and session types first so everything else is
|
|
26
|
+
* inferred from the options.
|
|
27
|
+
*/
|
|
28
|
+
export declare class LambderMockApp<C extends LambderApiContractShape, S = any, G extends Record<string, LambderApiGuard<any, any, any>> = {}> {
|
|
29
|
+
readonly apiVersion: string | null;
|
|
30
|
+
/** The memory stores, for assertions and reset; null for a subsystem that is off or backed by a store of yours. */
|
|
31
|
+
readonly sessionStore: LambderMemorySessionStore<S> | null;
|
|
32
|
+
readonly rateLimiter: LambderMemoryRateLimiter | null;
|
|
33
|
+
readonly idempotencyStore: LambderMemoryIdempotencyStore | null;
|
|
34
|
+
readonly tokenCookieKey: string;
|
|
35
|
+
readonly csrfCookieKey: string;
|
|
36
|
+
/**
|
|
37
|
+
* The client IP a transport request carrying none is read as. Public
|
|
38
|
+
* because an adapter has to read the same default the direct transport
|
|
39
|
+
* uses: the MSW adapter had a hardcoded "127.0.0.1" of its own, so an app
|
|
40
|
+
* that set defaultClientIp saw one address through the transport and
|
|
41
|
+
* another through the service worker, and a per-IP rate limit counted two
|
|
42
|
+
* clients where there was one.
|
|
43
|
+
*/
|
|
44
|
+
readonly defaultClientIp: string;
|
|
45
|
+
/** The host this runtime's cookies belong to (see the cookieHost option). */
|
|
46
|
+
readonly cookieHost: string;
|
|
47
|
+
/**
|
|
48
|
+
* The API core, every protocol step of it. Private: the mock's surface is
|
|
49
|
+
* the app, and a consumer reaching past it would be configuring the
|
|
50
|
+
* server's pipeline through a development tool. The three adapters take
|
|
51
|
+
* what they need from the app's own methods (handleRequest,
|
|
52
|
+
* requestFromTransport), which is why none of them names this.
|
|
53
|
+
*/
|
|
54
|
+
private readonly pipeline;
|
|
55
|
+
/** The cookie scope signIn plants under, so signOut can name the same one when it clears them. */
|
|
56
|
+
private readonly sessionCookieOptions;
|
|
57
|
+
private readonly sessionTtlSeconds;
|
|
58
|
+
private readonly onReset;
|
|
59
|
+
private readonly revealHandlerErrors;
|
|
60
|
+
/** Injected failures, the offline switch and the configured latency (see LambderMockFailureInjector). */
|
|
61
|
+
private readonly failures;
|
|
62
|
+
/** Subscriptions and the bounded call log (see LambderMockCallRecorder). */
|
|
63
|
+
private readonly recorder;
|
|
64
|
+
/** Registered entries and the overrides over them (see LambderMockEntryRegistry). */
|
|
65
|
+
private readonly registry;
|
|
66
|
+
/** The jars the runtime owns and what it planted in document.cookie (see LambderMockBrowserCookies). */
|
|
67
|
+
private readonly browserCookies;
|
|
68
|
+
constructor(options: LambderMockAppOptions<C, S, G>);
|
|
69
|
+
/**
|
|
70
|
+
* The registration-time checks every entry goes through, mocked or not:
|
|
71
|
+
* the ones the server runs on a definition, and the mock's own "a session
|
|
72
|
+
* endpoint needs the sessions option".
|
|
73
|
+
*
|
|
74
|
+
* One place, so no registration path can skip either check. A session
|
|
75
|
+
* endpoint on a mock without sessions would otherwise register silently
|
|
76
|
+
* and answer the first call with a 500 from inside the pipeline, naming
|
|
77
|
+
* the SERVER's option name, for a mistake whose fix is one option at
|
|
78
|
+
* create().
|
|
79
|
+
*/
|
|
80
|
+
private assertEntryRegistration;
|
|
81
|
+
private buildEntry;
|
|
82
|
+
/** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
|
|
83
|
+
publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema>): LambderMockEntry<C, K>;
|
|
84
|
+
/** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
|
|
85
|
+
sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema>): LambderMockEntry<C, K>;
|
|
86
|
+
/**
|
|
87
|
+
* A public endpoint deliberately left without a mock; a call answers the
|
|
88
|
+
* notMocked refusal carrying the reason.
|
|
89
|
+
*
|
|
90
|
+
* Public and session have separate builders for the same reason publicApi
|
|
91
|
+
* and sessionApi do: the refusal runs through the pipeline so that the
|
|
92
|
+
* steps BEFORE dispatch still happen, and the session read is one of them.
|
|
93
|
+
* Declaring every not-mocked endpoint public switched that step off, so a
|
|
94
|
+
* session endpoint with no session answered "not mocked" where the server
|
|
95
|
+
* answers sessionExpired, and the mode on its events and call log was
|
|
96
|
+
* wrong too. The mode cannot be recovered at runtime, because the contract
|
|
97
|
+
* is a type, so the builder is where it has to be said.
|
|
98
|
+
*/
|
|
99
|
+
notMocked<K extends LambderMockPublicNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
|
|
100
|
+
/** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
|
|
101
|
+
sessionNotMocked<K extends LambderMockSessionNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
|
|
102
|
+
/**
|
|
103
|
+
* "Everything I did not register is not mocked, for this reason", as an
|
|
104
|
+
* argument to the same register() call:
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* mockApp.register(userMocks, billingMocks, mockApp.restNotMocked("not mocked yet"));
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* What it buys is adoption over a contract the mocks do not cover yet:
|
|
111
|
+
* register() stays exhaustive by construction, and the endpoints nothing
|
|
112
|
+
* claims answer the notMocked refusal carrying this reason instead of
|
|
113
|
+
* apiNotFound, so a screen that reaches one says "not mocked yet" rather
|
|
114
|
+
* than "unknown error". Strays and duplicates in the explicit slices are
|
|
115
|
+
* refused exactly as they are without it, and an entry registered later
|
|
116
|
+
* (registerPartial, or a second register) takes the endpoint back from the
|
|
117
|
+
* rest.
|
|
118
|
+
*
|
|
119
|
+
* The one thing it cannot do is the session read. A call it answers is
|
|
120
|
+
* processed as a public endpoint: the protocol's pre-pass still runs, so a
|
|
121
|
+
* stale client still hears versionExpired, but the mode of a name nothing
|
|
122
|
+
* registered is not knowable at runtime, the contract being a type. So a
|
|
123
|
+
* signed-out call to an unmocked session endpoint is answered "not mocked"
|
|
124
|
+
* where the server answers sessionExpired, and the endpoint whose
|
|
125
|
+
* signed-out path a test cares about is the one to declare with
|
|
126
|
+
* sessionNotMocked instead.
|
|
127
|
+
*/
|
|
128
|
+
restNotMocked(reason: string): LambderMockRestEntry;
|
|
129
|
+
private buildNotMockedEntry;
|
|
130
|
+
/** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
|
|
131
|
+
apiSlice<const E extends readonly LambderMockEntry<C, keyof C & string>[]>(...entries: E): LambderMockSlice<C, E[number]["name"]>;
|
|
132
|
+
private addSlices;
|
|
133
|
+
/**
|
|
134
|
+
* Registers the whole contract: every endpoint in exactly one slice, or
|
|
135
|
+
* in the reach of a restNotMocked entry passed beside them.
|
|
136
|
+
* Completeness, strays and overlap are checked by the compiler against
|
|
137
|
+
* the contract type; overlap and key-to-name agreement are checked again
|
|
138
|
+
* at runtime for slices built dynamically, and a second rest entry is
|
|
139
|
+
* refused there the way a duplicate name is.
|
|
140
|
+
*/
|
|
141
|
+
register<const Slices extends readonly (Record<string, LambderMockEntry<C, any>> | LambderMockRestEntry)[]>(...slices: Slices & LambderMockRegistryCheck<C, Slices>): this;
|
|
142
|
+
/** Registers some endpoints, for a test that wants three and not three hundred. Overlap is still an error. */
|
|
143
|
+
registerPartial(...slices: readonly Record<string, LambderMockEntry<C, any>>[]): this;
|
|
144
|
+
/**
|
|
145
|
+
* Replaces one endpoint's handler until restored: returns its own undo,
|
|
146
|
+
* which a test scopes with try/finally. The entry's declarations (mode,
|
|
147
|
+
* guards, rate limit, idempotency) stay as registered; only the handler
|
|
148
|
+
* changes.
|
|
149
|
+
*
|
|
150
|
+
* Overrides nest. A second override over the same endpoint stands on the
|
|
151
|
+
* first, and restoring it uncovers the first rather than the registry, so
|
|
152
|
+
* an override one `it` scoped cannot drop the one a describe put in place
|
|
153
|
+
* around it.
|
|
154
|
+
*/
|
|
155
|
+
override<K extends keyof C & string>(name: K, handler: LambderMockHandler<C, K, S, G>): LambderMockOverride;
|
|
156
|
+
/** Puts every overridden handler back, however deeply they were stacked. */
|
|
157
|
+
restoreOverrides(): void;
|
|
158
|
+
/** The registered endpoint names. */
|
|
159
|
+
get registeredNames(): string[];
|
|
160
|
+
/**
|
|
161
|
+
* Whether a call to this name would be answered from the registry, which
|
|
162
|
+
* is what an adapter asks before passing one on.
|
|
163
|
+
*
|
|
164
|
+
* True for every name once a rest entry is registered, because the rest
|
|
165
|
+
* entry is what answers the names nothing else claimed. That is what makes
|
|
166
|
+
* a rest entry and the MSW adapter's `onUnmocked: "passthrough"`
|
|
167
|
+
* alternatives rather than layers: with one registered, the runtime
|
|
168
|
+
* answers everything itself and nothing is handed on to the network.
|
|
169
|
+
*/
|
|
170
|
+
hasRegisteredEntry(apiName: string): boolean;
|
|
171
|
+
private entryFor;
|
|
172
|
+
/**
|
|
173
|
+
* The entry that answers a name nothing registered, when register() was
|
|
174
|
+
* given a rest entry: the notMocked refusal carrying its reason, run
|
|
175
|
+
* through the pipeline as a public endpoint.
|
|
176
|
+
*
|
|
177
|
+
* Public because the mode of an unregistered name cannot be recovered at
|
|
178
|
+
* runtime, the contract being a type. Everything that precedes dispatch
|
|
179
|
+
* still runs (the version gate, the payload restore); the session read is
|
|
180
|
+
* the one step this answer cannot have, which is the fidelity limit
|
|
181
|
+
* restNotMocked documents.
|
|
182
|
+
*/
|
|
183
|
+
private restNotMockedEntry;
|
|
184
|
+
/** The next call to the endpoint fails this way; several calls queue in order. */
|
|
185
|
+
failNext(apiName: keyof C & string, failure: LambderMockFailure | LambderMockFailureReason): void;
|
|
186
|
+
/** Every call to the endpoint fails this way until cleared with null. */
|
|
187
|
+
setFailure(apiName: keyof C & string, failure: LambderMockFailure | LambderMockFailureReason | null): void;
|
|
188
|
+
/** Every call rejects at the transport, as with no network at all. */
|
|
189
|
+
setOffline(offline: boolean): void;
|
|
190
|
+
setLatency(latency: LambderMockLatency): void;
|
|
191
|
+
/**
|
|
192
|
+
* Rewinds the runtime: sessions, rate-limit counters, replay records,
|
|
193
|
+
* overrides, injected failures, the offline switch, the configured
|
|
194
|
+
* latency, the call log and its numbering, the cookies its own transports
|
|
195
|
+
* hold, then onReset, so the app rewinds its own data too.
|
|
196
|
+
*
|
|
197
|
+
* The cookies matter as much as the sessions do: emptying the session
|
|
198
|
+
* store while a jar still holds the token for one of them leaves the next
|
|
199
|
+
* call carrying a session that no longer exists, which reads as signed in
|
|
200
|
+
* until the answer says sessionExpired. So every jar transport() built
|
|
201
|
+
* for itself is emptied, and the cookies a "document" transport mirrored
|
|
202
|
+
* are expired again.
|
|
203
|
+
*
|
|
204
|
+
* The registry survives, being what the runtime was configured with
|
|
205
|
+
* rather than what it accumulated. Subscriptions survive too, because
|
|
206
|
+
* they are how a test watches the runtime rather than state it is
|
|
207
|
+
* testing; a listener muted for throwing is unmuted, so one bad call does
|
|
208
|
+
* not silence it for the rest of the run. A session store or a cookie jar
|
|
209
|
+
* the app supplied itself survives: the runtime did not create it and
|
|
210
|
+
* does not know what else holds it.
|
|
211
|
+
*/
|
|
212
|
+
reset(): void;
|
|
213
|
+
/**
|
|
214
|
+
* The four session members refuse in the mock's own words, naming the
|
|
215
|
+
* option a mock is created with.
|
|
216
|
+
*
|
|
217
|
+
* The pipeline's guard says "Configure the session option at creation",
|
|
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.
|
|
221
|
+
*/
|
|
222
|
+
private assertSessionsConfigured;
|
|
223
|
+
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when sessions are off. */
|
|
224
|
+
get sessionManager(): LambderSessionManager<S>;
|
|
225
|
+
/**
|
|
226
|
+
* Starts a session without a login endpoint: creates it through the
|
|
227
|
+
* session controller, the way a login handler does, plants its cookies
|
|
228
|
+
* into the jar when one is given, so the jar's transport is signed in
|
|
229
|
+
* from its next call, and mirrors the readable ones into document.cookie
|
|
230
|
+
* the way an answer's cookies are. Returns the raw tokens too.
|
|
231
|
+
*/
|
|
232
|
+
signIn(sessionKey: string, data: S, options?: {
|
|
233
|
+
jar?: LambderCookieJar;
|
|
234
|
+
ttlSeconds?: number;
|
|
235
|
+
host?: string;
|
|
236
|
+
}): Promise<LambderCreatedSession<S>>;
|
|
237
|
+
/**
|
|
238
|
+
* Ends every session of the subject ("log this subject out everywhere")
|
|
239
|
+
* and clears what signIn planted: the cookies in the jar given, and the
|
|
240
|
+
* copies in document.cookie.
|
|
241
|
+
*
|
|
242
|
+
* Symmetric on purpose, the way reset() is. The records alone leave the
|
|
243
|
+
* jar and the page carrying a token for a session that no longer exists,
|
|
244
|
+
* which reads as signed in until an answer says otherwise.
|
|
245
|
+
*/
|
|
246
|
+
signOut(sessionKey: string, options?: {
|
|
247
|
+
jar?: LambderCookieJar;
|
|
248
|
+
host?: string;
|
|
249
|
+
}): Promise<void>;
|
|
250
|
+
/** Marks the subject's session data stale, so the next read renews it through dataRefresh. */
|
|
251
|
+
expireSessionData(sessionKey: string): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Listens to every call, both phases. Keyed, so a hot-reloaded module
|
|
254
|
+
* replaces its own listener instead of stacking a duplicate. Returns the
|
|
255
|
+
* unsubscribe.
|
|
256
|
+
*/
|
|
257
|
+
subscribe(key: string, listener: LambderMockListener): () => void;
|
|
258
|
+
/** The completed calls, oldest first, bounded by callLogSize. */
|
|
259
|
+
get calls(): readonly LambderMockCallRecord[];
|
|
260
|
+
private emit;
|
|
261
|
+
/** The context one call runs on: the core's call context plus what mock guards and handlers see. */
|
|
262
|
+
private createContext;
|
|
263
|
+
/** What a call looks like on the way in, for the runtime's own calls and for one an adapter passes on. */
|
|
264
|
+
private requestEvent;
|
|
265
|
+
/**
|
|
266
|
+
* Records a call an adapter handed on instead of answering: the MSW
|
|
267
|
+
* adapter's passthrough. Without it a name the registry does not know
|
|
268
|
+
* leaves no trace at all, and a mistyped endpoint reaches the real
|
|
269
|
+
* network with nothing in the call log or on the subscription to say so,
|
|
270
|
+
* which is the one failure the log exists to make visible.
|
|
271
|
+
*/
|
|
272
|
+
notePassthrough(request: LambderApiRequest): void;
|
|
273
|
+
/** What every record of one call repeats (see LambderMockCallFacts). */
|
|
274
|
+
private callFacts;
|
|
275
|
+
/** One call from a parsed request to its answer, events included. The entry point every transport and adapter shares. */
|
|
276
|
+
handleRequest(request: LambderApiRequest): Promise<LambderApiAnswer>;
|
|
277
|
+
/**
|
|
278
|
+
* A transport request as the core's request: the envelope read the way the
|
|
279
|
+
* server reads it. Public because the adapters call it, which is what
|
|
280
|
+
* keeps them from each reading a transport request their own way.
|
|
281
|
+
*/
|
|
282
|
+
requestFromTransport(transportRequest: LambderApiTransportRequest): LambderApiRequest;
|
|
283
|
+
/** One call from a transport request to its answer: what the mock transport and the adapters call. */
|
|
284
|
+
handle(transportRequest: LambderApiTransportRequest): Promise<LambderApiAnswer>;
|
|
285
|
+
/**
|
|
286
|
+
* The direct transport: a caller's request into handle(), the answer
|
|
287
|
+
* back in the form the caller reads, cookies carried by a jar the way
|
|
288
|
+
* a browser carries them. Each transport gets its own jar unless one is
|
|
289
|
+
* given, so two transports hold two sessions; the jar is on the returned
|
|
290
|
+
* transport as `cookieJar`, so a test can read or clear the one it did
|
|
291
|
+
* not create itself, and reset() empties it.
|
|
292
|
+
*/
|
|
293
|
+
transport(options?: LambderMockTransportOptions): LambderMockTransport;
|
|
294
|
+
/**
|
|
295
|
+
* Mirrors an answer's non-HttpOnly cookies into document.cookie and
|
|
296
|
+
* remembers them, so reset() expires them again. The direct transport's
|
|
297
|
+
* "document" mode and the MSW adapter both come through here: one
|
|
298
|
+
* implementation of the mirror, one record of what was planted.
|
|
299
|
+
*/
|
|
300
|
+
mirrorCookiesIntoDocument(setCookies: readonly string[]): void;
|
|
301
|
+
/**
|
|
302
|
+
* Takes a jar an adapter built for itself as the runtime's own, so reset()
|
|
303
|
+
* empties it with the rest. The MSW adapter's jar holds the session
|
|
304
|
+
* cookies of calls that never touch transport(), and a reset that leaves
|
|
305
|
+
* it full is the same stale-session bug: the store is empty and the next
|
|
306
|
+
* request still carries a token for one of its sessions.
|
|
307
|
+
*/
|
|
308
|
+
adoptCookieJar(jar: LambderCookieJar): void;
|
|
309
|
+
/** caller.setTransport(mockApp.transport(options)); returns the transport, its jar on it. */
|
|
310
|
+
attach(caller: {
|
|
311
|
+
setTransport(transport: LambderApiTransport): unknown;
|
|
312
|
+
}, options?: LambderMockTransportOptions): LambderMockTransport;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Fixes the contract and session data types, then hands out the guard
|
|
316
|
+
* builder bound to the mock's contexts and the create() that infers
|
|
317
|
+
* everything else (the guard map, the rate-limit policies) from its options.
|
|
318
|
+
* Curried for the same reason initLambder is: TypeScript type arguments are
|
|
319
|
+
* all-or-nothing per call.
|
|
320
|
+
*
|
|
321
|
+
* ```ts
|
|
322
|
+
* const mock = initLambderMock<ApiContractType, SessionData>();
|
|
323
|
+
* const mockApp = mock.create({
|
|
324
|
+
* apiVersion: "1.4.0",
|
|
325
|
+
* sessions: true,
|
|
326
|
+
* guards: { tenant: mock.guard({ guardInput: z.object({ tenantId: z.uuid() }), session: true, handler: ... }) },
|
|
327
|
+
* });
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export declare const initLambderMock: <C extends LambderApiContractShape, S = any>() => {
|
|
331
|
+
/** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
|
|
332
|
+
guard: LambderGuardBuilder<LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
|
|
333
|
+
/**
|
|
334
|
+
* Builds a mock rate-limit key, the counterpart of `guard`. Bound to the
|
|
335
|
+
* mock's own call context, because the server's lambderRateLimitKey() is
|
|
336
|
+
* bound to the render context and a handler written with it compiles here
|
|
337
|
+
* while reading fields the mock context does not have.
|
|
338
|
+
*/
|
|
339
|
+
rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderMockCallContext<S>>;
|
|
340
|
+
/**
|
|
341
|
+
* The mock app, with the guard map and the rate-limit policies inferred
|
|
342
|
+
* from the options.
|
|
343
|
+
*
|
|
344
|
+
* `const` on each of them is what pins a restatement to the contract, and
|
|
345
|
+
* it has one cost: inferring a generic from an object literal switches
|
|
346
|
+
* excess-property checking off for the whole literal, nested objects
|
|
347
|
+
* included, so a typo inside `rateLimits.policies` or `idempotency`
|
|
348
|
+
* compiled and was dropped in silence. `I` exists for the same reason `P`
|
|
349
|
+
* does, and LambderMockSurplusKeys puts the error back on the key.
|
|
350
|
+
*/
|
|
351
|
+
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
|
+
};
|