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,55 @@
|
|
|
1
|
+
import { type LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where the runtime's cookies live outside its own answers: the jars it built
|
|
4
|
+
* for itself, and the copies it planted in the page's own cookie storage.
|
|
5
|
+
*
|
|
6
|
+
* The fourth of LambderMockApp's collaborators, and the same argument as the
|
|
7
|
+
* other three: it owns state nothing else touches and meets the runtime at two
|
|
8
|
+
* calls (a transport's answer coming back, and reset()). It is the piece with
|
|
9
|
+
* an outside dependency, `document`, so keeping it here is also what keeps the
|
|
10
|
+
* app free of browser conditionals.
|
|
11
|
+
*
|
|
12
|
+
* Both the direct transport's "document" mode and the MSW adapter come through
|
|
13
|
+
* one instance, so there is one mirror implementation and one record of what
|
|
14
|
+
* was planted. They carried a copy each before: the MSW copy dropped `Secure`
|
|
15
|
+
* on a page that is not a secure context and the transport's did not, so on
|
|
16
|
+
* plain http (device testing on a LAN address) the browser silently refused
|
|
17
|
+
* the CSRF cookie and every session call failed its CSRF check with nothing in
|
|
18
|
+
* any log to say why.
|
|
19
|
+
*/
|
|
20
|
+
export declare class LambderMockBrowserCookies {
|
|
21
|
+
/** The jars transport() and the adapters built for themselves, which reset() is therefore free to empty. */
|
|
22
|
+
private readonly ownedJars;
|
|
23
|
+
/** What was mirrored into document.cookie, so reset() can expire exactly those. */
|
|
24
|
+
private readonly mirroredCookies;
|
|
25
|
+
/**
|
|
26
|
+
* Takes a jar built for the runtime as the runtime's own, so reset()
|
|
27
|
+
* empties it with the rest. A jar the app passed in stays the app's, the
|
|
28
|
+
* way an app-supplied session store does: the runtime did not create it and
|
|
29
|
+
* does not know what else holds it.
|
|
30
|
+
*/
|
|
31
|
+
adoptJar(jar: LambderCookieJar): void;
|
|
32
|
+
/**
|
|
33
|
+
* Mirrors an answer's non-HttpOnly cookies into document.cookie and
|
|
34
|
+
* remembers them for reset().
|
|
35
|
+
*
|
|
36
|
+
* HttpOnly cookies are skipped exactly as a real browser skips them, the
|
|
37
|
+
* jar being the store no script can reach. `Secure` is dropped where the
|
|
38
|
+
* page is not a secure context, because the browser would refuse the write
|
|
39
|
+
* and development over plain http on a LAN address has to keep working. A
|
|
40
|
+
* `__Host-` or `__Secure-` cookie name is then discarded by the browser
|
|
41
|
+
* for breaking its own prefix rule, which is correct: such a name cannot
|
|
42
|
+
* work on plain http at all, and localhost is a secure context.
|
|
43
|
+
*/
|
|
44
|
+
mirrorSetCookies(setCookies: readonly string[]): void;
|
|
45
|
+
/**
|
|
46
|
+
* Empties the jars the runtime owns and expires what it mirrored, which is
|
|
47
|
+
* what clearing the page's cookie storage would do.
|
|
48
|
+
*
|
|
49
|
+
* As much a part of a rewind as the session store is: emptying the store
|
|
50
|
+
* while a jar still holds the token for one of its sessions leaves the next
|
|
51
|
+
* call carrying a session that no longer exists, which reads as signed in
|
|
52
|
+
* until the answer says sessionExpired.
|
|
53
|
+
*/
|
|
54
|
+
reset(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseSetCookie } from "../shared/transport/LambderCookieJar.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where the runtime's cookies live outside its own answers: the jars it built
|
|
4
|
+
* for itself, and the copies it planted in the page's own cookie storage.
|
|
5
|
+
*
|
|
6
|
+
* The fourth of LambderMockApp's collaborators, and the same argument as the
|
|
7
|
+
* other three: it owns state nothing else touches and meets the runtime at two
|
|
8
|
+
* calls (a transport's answer coming back, and reset()). It is the piece with
|
|
9
|
+
* an outside dependency, `document`, so keeping it here is also what keeps the
|
|
10
|
+
* app free of browser conditionals.
|
|
11
|
+
*
|
|
12
|
+
* Both the direct transport's "document" mode and the MSW adapter come through
|
|
13
|
+
* one instance, so there is one mirror implementation and one record of what
|
|
14
|
+
* was planted. They carried a copy each before: the MSW copy dropped `Secure`
|
|
15
|
+
* on a page that is not a secure context and the transport's did not, so on
|
|
16
|
+
* plain http (device testing on a LAN address) the browser silently refused
|
|
17
|
+
* the CSRF cookie and every session call failed its CSRF check with nothing in
|
|
18
|
+
* any log to say why.
|
|
19
|
+
*/
|
|
20
|
+
export class LambderMockBrowserCookies {
|
|
21
|
+
/** The jars transport() and the adapters built for themselves, which reset() is therefore free to empty. */
|
|
22
|
+
ownedJars = new Set();
|
|
23
|
+
/** What was mirrored into document.cookie, so reset() can expire exactly those. */
|
|
24
|
+
mirroredCookies = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Takes a jar built for the runtime as the runtime's own, so reset()
|
|
27
|
+
* empties it with the rest. A jar the app passed in stays the app's, the
|
|
28
|
+
* way an app-supplied session store does: the runtime did not create it and
|
|
29
|
+
* does not know what else holds it.
|
|
30
|
+
*/
|
|
31
|
+
adoptJar(jar) {
|
|
32
|
+
this.ownedJars.add(jar);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Mirrors an answer's non-HttpOnly cookies into document.cookie and
|
|
36
|
+
* remembers them for reset().
|
|
37
|
+
*
|
|
38
|
+
* HttpOnly cookies are skipped exactly as a real browser skips them, the
|
|
39
|
+
* jar being the store no script can reach. `Secure` is dropped where the
|
|
40
|
+
* page is not a secure context, because the browser would refuse the write
|
|
41
|
+
* and development over plain http on a LAN address has to keep working. A
|
|
42
|
+
* `__Host-` or `__Secure-` cookie name is then discarded by the browser
|
|
43
|
+
* for breaking its own prefix rule, which is correct: such a name cannot
|
|
44
|
+
* work on plain http at all, and localhost is a secure context.
|
|
45
|
+
*/
|
|
46
|
+
mirrorSetCookies(setCookies) {
|
|
47
|
+
if (typeof document === "undefined")
|
|
48
|
+
return;
|
|
49
|
+
const secureContext = typeof globalThis.isSecureContext === "boolean" ? globalThis.isSecureContext : true;
|
|
50
|
+
for (const header of setCookies) {
|
|
51
|
+
const cookie = parseSetCookie(header, Date.now());
|
|
52
|
+
if (!cookie || cookie.httpOnly)
|
|
53
|
+
continue;
|
|
54
|
+
document.cookie = secureContext ? header : header.replace(/;\s*Secure\b/i, "");
|
|
55
|
+
this.mirroredCookies.set(`${cookie.name}|${cookie.path}`, { name: cookie.name, path: cookie.path });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Empties the jars the runtime owns and expires what it mirrored, which is
|
|
60
|
+
* what clearing the page's cookie storage would do.
|
|
61
|
+
*
|
|
62
|
+
* As much a part of a rewind as the session store is: emptying the store
|
|
63
|
+
* while a jar still holds the token for one of its sessions leaves the next
|
|
64
|
+
* call carrying a session that no longer exists, which reads as signed in
|
|
65
|
+
* until the answer says sessionExpired.
|
|
66
|
+
*/
|
|
67
|
+
reset() {
|
|
68
|
+
for (const jar of this.ownedJars)
|
|
69
|
+
jar.clear();
|
|
70
|
+
if (typeof document !== "undefined") {
|
|
71
|
+
for (const cookie of this.mirroredCookies.values())
|
|
72
|
+
document.cookie = `${cookie.name}=; Path=${cookie.path}; Max-Age=0`;
|
|
73
|
+
}
|
|
74
|
+
this.mirroredCookies.clear();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
|
|
2
|
+
import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
|
|
3
|
+
import type { LambderMockCallEvent, LambderMockCallRecord, LambderMockListener, LambderMockOutcome } from "./LambderMockTypes.js";
|
|
4
|
+
/** What every record of one call repeats: who called what, and when it started. */
|
|
5
|
+
export type LambderMockCallFacts = {
|
|
6
|
+
id: number;
|
|
7
|
+
apiName: string;
|
|
8
|
+
/** The endpoint's mode from the registry; null for a name the registry does not know. */
|
|
9
|
+
mode: LambderApiMode | null;
|
|
10
|
+
/** When the call started, which is what its duration is measured from. */
|
|
11
|
+
startedAt: number;
|
|
12
|
+
/**
|
|
13
|
+
* The call's request, read when the call settles rather than copied when it
|
|
14
|
+
* starts. The pipeline rewrites the payload as the call goes: a compressed
|
|
15
|
+
* one is restored before anything reads it, and an endpoint with an input
|
|
16
|
+
* schema replaces it with the parsed value. Copied up front, the log kept
|
|
17
|
+
* the wire fields, so the compressed calls a developer opens a panel for
|
|
18
|
+
* were the ones logged as `undefined`.
|
|
19
|
+
*/
|
|
20
|
+
request: {
|
|
21
|
+
payload: unknown;
|
|
22
|
+
guardInputs: Record<string, unknown> | undefined;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** How a call ended, as the runtime saw it happen. */
|
|
26
|
+
type LambderMockCallEnding = {
|
|
27
|
+
/** The answer the caller receives; null when there is none: a rejected transport, or a passthrough. */
|
|
28
|
+
answer: LambderApiAnswer | null;
|
|
29
|
+
/** The outcome where the runtime already knows it (injected, replayed, passthrough); read off the answer otherwise, and required when there is no answer to read. */
|
|
30
|
+
outcome?: LambderMockOutcome;
|
|
31
|
+
guardsRun: readonly string[];
|
|
32
|
+
/** The crash, or the transport failure. */
|
|
33
|
+
error?: Error;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What the runtime lets someone watch: the keyed subscriptions every call is
|
|
37
|
+
* emitted to, and the bounded log of completed calls.
|
|
38
|
+
*
|
|
39
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
40
|
+
* touches.
|
|
41
|
+
* `subscribe` and `calls` stay on the app as one-line delegations, because
|
|
42
|
+
* they are the surface a dev panel reads.
|
|
43
|
+
*
|
|
44
|
+
* Ending a call is `settle`, one call for the whole of it: classification,
|
|
45
|
+
* redaction, the event and the log row. A record literal built at each of the
|
|
46
|
+
* three exits instead would drift between them, which is exactly what a log
|
|
47
|
+
* is read to rule out.
|
|
48
|
+
*
|
|
49
|
+
* `calls` hands out copies down to the values, so a reader that sorts
|
|
50
|
+
* guardsRun or deletes a header is not editing what the next reader sees, and
|
|
51
|
+
* the caller is free to edit what it got. The Error is the exception, passed
|
|
52
|
+
* by reference: a clone of it would no longer be the class a test asserts on,
|
|
53
|
+
* and an Error carries nothing worth protecting.
|
|
54
|
+
*/
|
|
55
|
+
export declare class LambderMockCallRecorder {
|
|
56
|
+
private readonly listeners;
|
|
57
|
+
/** Listeners that threw, skipped until they subscribe again or the runtime resets. */
|
|
58
|
+
private readonly mutedListeners;
|
|
59
|
+
private readonly callLog;
|
|
60
|
+
private readonly callLogSize;
|
|
61
|
+
private callSequence;
|
|
62
|
+
constructor(options: {
|
|
63
|
+
callLogSize: number;
|
|
64
|
+
});
|
|
65
|
+
/** The id of the next call, which numbers its request and response events alike. */
|
|
66
|
+
nextCallId(): number;
|
|
67
|
+
subscribe(key: string, listener: LambderMockListener): () => void;
|
|
68
|
+
/** The completed calls, oldest first, bounded by callLogSize. */
|
|
69
|
+
get calls(): readonly LambderMockCallRecord[];
|
|
70
|
+
emit(event: LambderMockCallEvent): void;
|
|
71
|
+
/**
|
|
72
|
+
* Ends one call: reads how it went, redacts what a log has no business
|
|
73
|
+
* keeping, and emits the response event and the log row from one object,
|
|
74
|
+
* so the two cannot say different things about the same call.
|
|
75
|
+
*
|
|
76
|
+
* The event and the row carry copies of their own, so a listener that
|
|
77
|
+
* edits the event it was handed is not editing the row the log keeps; the
|
|
78
|
+
* row is copied again on the way in and on the way out.
|
|
79
|
+
*/
|
|
80
|
+
settle(facts: LambderMockCallFacts, ending: LambderMockCallEnding): void;
|
|
81
|
+
private push;
|
|
82
|
+
/** Empties the log and its numbering, and unmutes listeners; the subscriptions themselves survive. */
|
|
83
|
+
reset(): void;
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
|
|
2
|
+
/**
|
|
3
|
+
* Set-Cookie values are redacted in the log: the name stays so a reader can
|
|
4
|
+
* see that a session cookie was written, the value goes, because a live
|
|
5
|
+
* session token in a panel a developer renders and a test snapshots is no
|
|
6
|
+
* place to keep it.
|
|
7
|
+
*/
|
|
8
|
+
const loggedHeaders = (headers) => {
|
|
9
|
+
const copy = {};
|
|
10
|
+
for (const [key, values] of Object.entries(headers)) {
|
|
11
|
+
copy[key] = key.toLowerCase() === "set-cookie"
|
|
12
|
+
? values.map((header) => header.replace(/^([^=;]+)=[^;]*/, "$1=[redacted]"))
|
|
13
|
+
: [...values];
|
|
14
|
+
}
|
|
15
|
+
return copy;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A logged value copied, so a reader that reaches into a record cannot edit
|
|
19
|
+
* what the next reader sees. structuredClone is the platform's deep copy and
|
|
20
|
+
* everything logged arrived as JSON; a value it refuses (a function on the
|
|
21
|
+
* payload of a hand-built request) is handed over as it is rather than
|
|
22
|
+
* failing the read.
|
|
23
|
+
*/
|
|
24
|
+
const cloneLoggedValue = (value) => {
|
|
25
|
+
if (value === null || typeof value !== "object")
|
|
26
|
+
return value;
|
|
27
|
+
try {
|
|
28
|
+
return structuredClone(value);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** The envelope an answer carries, when it carries one. */
|
|
35
|
+
const envelopeOf = (answer) => {
|
|
36
|
+
if (answer.isBodyBase64)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(answer.body);
|
|
40
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/** How an answer reads, in the runtime's vocabulary. */
|
|
47
|
+
const classifyAnswer = (answer, envelope) => {
|
|
48
|
+
if (answer.statusCode >= 500)
|
|
49
|
+
return "crash";
|
|
50
|
+
if (answer.statusCode === 422)
|
|
51
|
+
return "validation";
|
|
52
|
+
if (!envelope)
|
|
53
|
+
return "ok";
|
|
54
|
+
if (envelope.versionExpired)
|
|
55
|
+
return "versionExpired";
|
|
56
|
+
if (envelope.sessionExpired)
|
|
57
|
+
return "sessionExpired";
|
|
58
|
+
if (envelope.notAuthorized)
|
|
59
|
+
return "notAuthorized";
|
|
60
|
+
const code = envelope.errorMessage?.code;
|
|
61
|
+
if (code === LAMBDER_REFUSAL_CODES.rateLimited)
|
|
62
|
+
return "rateLimited";
|
|
63
|
+
if (code === LAMBDER_REFUSAL_CODES.notMocked)
|
|
64
|
+
return "notMocked";
|
|
65
|
+
if (code === LAMBDER_REFUSAL_CODES.apiNotFound)
|
|
66
|
+
return "unknownApi";
|
|
67
|
+
if (envelope.errorMessage)
|
|
68
|
+
return "refusal";
|
|
69
|
+
return "ok";
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* What the runtime lets someone watch: the keyed subscriptions every call is
|
|
73
|
+
* emitted to, and the bounded log of completed calls.
|
|
74
|
+
*
|
|
75
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
76
|
+
* touches.
|
|
77
|
+
* `subscribe` and `calls` stay on the app as one-line delegations, because
|
|
78
|
+
* they are the surface a dev panel reads.
|
|
79
|
+
*
|
|
80
|
+
* Ending a call is `settle`, one call for the whole of it: classification,
|
|
81
|
+
* redaction, the event and the log row. A record literal built at each of the
|
|
82
|
+
* three exits instead would drift between them, which is exactly what a log
|
|
83
|
+
* is read to rule out.
|
|
84
|
+
*
|
|
85
|
+
* `calls` hands out copies down to the values, so a reader that sorts
|
|
86
|
+
* guardsRun or deletes a header is not editing what the next reader sees, and
|
|
87
|
+
* the caller is free to edit what it got. The Error is the exception, passed
|
|
88
|
+
* by reference: a clone of it would no longer be the class a test asserts on,
|
|
89
|
+
* and an Error carries nothing worth protecting.
|
|
90
|
+
*/
|
|
91
|
+
export class LambderMockCallRecorder {
|
|
92
|
+
listeners = new Map();
|
|
93
|
+
/** Listeners that threw, skipped until they subscribe again or the runtime resets. */
|
|
94
|
+
mutedListeners = new Set();
|
|
95
|
+
callLog = [];
|
|
96
|
+
callLogSize;
|
|
97
|
+
callSequence = 0;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.callLogSize = options.callLogSize;
|
|
100
|
+
}
|
|
101
|
+
/** The id of the next call, which numbers its request and response events alike. */
|
|
102
|
+
nextCallId() { return ++this.callSequence; }
|
|
103
|
+
subscribe(key, listener) {
|
|
104
|
+
this.listeners.set(key, listener);
|
|
105
|
+
// Subscribing again is how a muted listener comes back: a hot reload
|
|
106
|
+
// replaces the broken panel with the fixed one under the same key.
|
|
107
|
+
this.mutedListeners.delete(key);
|
|
108
|
+
return () => { if (this.listeners.get(key) === listener)
|
|
109
|
+
this.listeners.delete(key); };
|
|
110
|
+
}
|
|
111
|
+
/** The completed calls, oldest first, bounded by callLogSize. */
|
|
112
|
+
get calls() {
|
|
113
|
+
return this.callLog.map((record) => ({
|
|
114
|
+
...record,
|
|
115
|
+
headers: cloneLoggedValue(record.headers),
|
|
116
|
+
guardsRun: [...record.guardsRun],
|
|
117
|
+
envelope: cloneLoggedValue(record.envelope),
|
|
118
|
+
payload: cloneLoggedValue(record.payload),
|
|
119
|
+
guardInputs: cloneLoggedValue(record.guardInputs),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
emit(event) {
|
|
123
|
+
for (const [key, listener] of this.listeners) {
|
|
124
|
+
if (this.mutedListeners.has(key))
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
listener(event);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Muted, not merely reported once: a listener that throws on
|
|
131
|
+
// one event throws on the next, so leaving it in the loop
|
|
132
|
+
// costs every remaining call a thrown error and a swallowed
|
|
133
|
+
// one, for a listener that is already known to be broken.
|
|
134
|
+
this.mutedListeners.add(key);
|
|
135
|
+
console.error(`[lambder mock] listener "${key}" threw and is muted until it subscribes again or the mock is reset`, err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Ends one call: reads how it went, redacts what a log has no business
|
|
141
|
+
* keeping, and emits the response event and the log row from one object,
|
|
142
|
+
* so the two cannot say different things about the same call.
|
|
143
|
+
*
|
|
144
|
+
* The event and the row carry copies of their own, so a listener that
|
|
145
|
+
* edits the event it was handed is not editing the row the log keeps; the
|
|
146
|
+
* row is copied again on the way in and on the way out.
|
|
147
|
+
*/
|
|
148
|
+
settle(facts, ending) {
|
|
149
|
+
const answer = ending.answer;
|
|
150
|
+
const envelope = answer ? envelopeOf(answer) : null;
|
|
151
|
+
const settledAt = Date.now();
|
|
152
|
+
const event = {
|
|
153
|
+
phase: "response",
|
|
154
|
+
id: facts.id, apiName: facts.apiName, mode: facts.mode,
|
|
155
|
+
durationMs: settledAt - facts.startedAt,
|
|
156
|
+
statusCode: answer?.statusCode ?? null,
|
|
157
|
+
headers: answer ? loggedHeaders(answer.headers) : {},
|
|
158
|
+
envelope,
|
|
159
|
+
outcome: ending.outcome ?? (answer ? classifyAnswer(answer, envelope) : "crash"),
|
|
160
|
+
guardsRun: [...ending.guardsRun],
|
|
161
|
+
...(ending.error ? { error: ending.error } : {}),
|
|
162
|
+
at: settledAt,
|
|
163
|
+
};
|
|
164
|
+
this.emit(event);
|
|
165
|
+
this.push({ ...event, guardsRun: [...ending.guardsRun], payload: facts.request.payload, guardInputs: facts.request.guardInputs });
|
|
166
|
+
}
|
|
167
|
+
push(record) {
|
|
168
|
+
this.callLog.push({
|
|
169
|
+
...record,
|
|
170
|
+
headers: cloneLoggedValue(record.headers),
|
|
171
|
+
envelope: cloneLoggedValue(record.envelope),
|
|
172
|
+
guardsRun: [...record.guardsRun],
|
|
173
|
+
});
|
|
174
|
+
if (this.callLog.length > this.callLogSize)
|
|
175
|
+
this.callLog.splice(0, this.callLog.length - this.callLogSize);
|
|
176
|
+
}
|
|
177
|
+
/** Empties the log and its numbering, and unmutes listeners; the subscriptions themselves survive. */
|
|
178
|
+
reset() {
|
|
179
|
+
this.callLog.length = 0;
|
|
180
|
+
this.callSequence = 0;
|
|
181
|
+
this.mutedListeners.clear();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { LambderContractGuardNames } from "../shared/wire/LambderApiContract.js";
|
|
2
|
+
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
3
|
+
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
4
|
+
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
5
|
+
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
6
|
+
import type { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
7
|
+
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
8
|
+
import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
|
|
9
|
+
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
10
|
+
import type { LambderSessionDataRefreshConfig } from "../session/LambderSessionManager.js";
|
|
11
|
+
import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
|
|
12
|
+
import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
|
|
13
|
+
import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSurplusKeys } from "./LambderMockTypes.js";
|
|
14
|
+
export type LambderMockSessionsOptions<S> = {
|
|
15
|
+
/** Where the mock's sessions rest. Default: a fresh LambderMemorySessionStore. */
|
|
16
|
+
store?: LambderSessionStore<S>;
|
|
17
|
+
/** Default: "lambder-mock". */
|
|
18
|
+
sessionSalt?: string;
|
|
19
|
+
/** TTL of sessions signIn() creates, in seconds. Default: 30 days. */
|
|
20
|
+
ttlSeconds?: number;
|
|
21
|
+
/** Hashing and randomness. Default: WebCrypto where the runtime offers it, the plain stand-in over a memory-only store otherwise. */
|
|
22
|
+
crypto?: LambderSessionCrypto;
|
|
23
|
+
dataRefresh?: LambderSessionDataRefreshConfig<S>;
|
|
24
|
+
enableSlidingExpiration?: boolean;
|
|
25
|
+
slidingWriteIntervalSeconds?: number;
|
|
26
|
+
tokenCookieKey?: string;
|
|
27
|
+
csrfCookieKey?: string;
|
|
28
|
+
cookieOptions?: LambderSessionCookieOptions;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Idempotency in the mock: the same engine the server runs, over a memory
|
|
32
|
+
* store unless one is given, and carrying every knob the server's own option
|
|
33
|
+
* carries.
|
|
34
|
+
*
|
|
35
|
+
* `callerIdentity` and `defaultPendingTtlSeconds` are here because a mock
|
|
36
|
+
* that cannot express them answers differently from the server on exactly the
|
|
37
|
+
* calls idempotency exists for: without an identity a public endpoint's stored
|
|
38
|
+
* answer replays to whoever presents the key, so a mock replayed where the
|
|
39
|
+
* server, configured with one, misses.
|
|
40
|
+
*/
|
|
41
|
+
export type LambderMockIdempotencyOptions<S = any> = {
|
|
42
|
+
/** Seconds a stored answer replays for. Default: 86400 (24h). Per-endpoint override: `idempotency: { ttlSeconds }`. */
|
|
43
|
+
defaultTtlSeconds?: number;
|
|
44
|
+
/** Seconds a claim stays pending before a retry may take the scope. Default: 300. Per-endpoint override: `idempotency: { pendingTtlSeconds }`. */
|
|
45
|
+
defaultPendingTtlSeconds?: number;
|
|
46
|
+
/** Run the handler when the store errors, instead of failing the call. Default: true. */
|
|
47
|
+
failOpen?: boolean;
|
|
48
|
+
/** Where replay records rest. Default: a fresh LambderMemoryIdempotencyStore. */
|
|
49
|
+
store?: LambderIdempotencyStore;
|
|
50
|
+
/**
|
|
51
|
+
* Who a call is acting as, for scoping a PUBLIC endpoint's stored answer;
|
|
52
|
+
* session endpoints already scope per session. The server's option word
|
|
53
|
+
* for word (see LambderApiIdempotencyConfig), bound to the mock's own call
|
|
54
|
+
* context, because that is the context the engine hands it here.
|
|
55
|
+
*
|
|
56
|
+
* Without the session, as on the server: this runs on public endpoints
|
|
57
|
+
* alone, where the session is always null, so offering it would be
|
|
58
|
+
* offering a field that answers nothing.
|
|
59
|
+
*/
|
|
60
|
+
callerIdentity?: (ctx: Omit<LambderMockCallContext<S>, "session">, request: LambderApiRequest) => string | null | Promise<string | null>;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The rate limits option: the policies endpoints may restate, the limiter
|
|
64
|
+
* they are counted on, and what happens when that limiter throws.
|
|
65
|
+
*
|
|
66
|
+
* `failOpen` is the server's own option (see LambderApiRateLimitsConfig) and
|
|
67
|
+
* is here for the reason the idempotency option's twin is: with a limiter of
|
|
68
|
+
* the app's own that fails, a mock that cannot express it always lets the
|
|
69
|
+
* call through, so it answers 200 where a server configured to refuse answers
|
|
70
|
+
* 429.
|
|
71
|
+
*/
|
|
72
|
+
type LambderMockRateLimitsOptions<S, P extends LambderMockRateLimitPolicies<S>> = {
|
|
73
|
+
policies: P & {
|
|
74
|
+
[N in keyof P]: LambderMockSurplusKeys<P[N], LambderApiRateLimitPolicyConfig<LambderMockCallContext<S>>>;
|
|
75
|
+
};
|
|
76
|
+
/** Where attempts are counted. Default: a fresh LambderMemoryRateLimiter. */
|
|
77
|
+
limiter?: LambderRateLimiter;
|
|
78
|
+
/** Let a call through when the limiter throws, instead of refusing it. Default: true. */
|
|
79
|
+
failOpen?: boolean;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The guards option: required whenever the contract declares any guard name,
|
|
83
|
+
* omittable only for a contract that declares none.
|
|
84
|
+
*
|
|
85
|
+
* The same reasoning the entry's own guards field carries: a guard the mock
|
|
86
|
+
* does not declare cannot run, and a call the server answers notAuthorized
|
|
87
|
+
* then answers 200 here. Optional, it was the droppable half of exactly the
|
|
88
|
+
* check it exists for.
|
|
89
|
+
*/
|
|
90
|
+
type LambderMockGuardsOption<C, S, G> = [
|
|
91
|
+
LambderContractGuardNames<C>
|
|
92
|
+
] extends [never] ? {
|
|
93
|
+
guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G>;
|
|
94
|
+
} : {
|
|
95
|
+
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G>;
|
|
96
|
+
};
|
|
97
|
+
/** Each guard checked for surplus keys, so `sesion: true` on an inline guard is an error at the key rather than a guard that silently runs as public. */
|
|
98
|
+
type LambderMockGuardShapes<S, G> = {
|
|
99
|
+
[N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
|
|
100
|
+
};
|
|
101
|
+
export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>> = LambderMockGuardsOption<C, S, G> & {
|
|
102
|
+
/** Enables the version gate: a call naming another version answers versionExpired, exactly as the server would. */
|
|
103
|
+
apiVersion?: string;
|
|
104
|
+
/** Artificial latency per call; off by default. */
|
|
105
|
+
latency?: LambderMockLatency;
|
|
106
|
+
/** Sessions over the memory store: `true` for the defaults, or the options. Off by default: session endpoints then fail at registration. */
|
|
107
|
+
sessions?: boolean | LambderMockSessionsOptions<S>;
|
|
108
|
+
/** The rate-limit policies endpoints may restate, over a memory limiter unless one is given. Off by default. */
|
|
109
|
+
rateLimits?: LambderMockRateLimitsOptions<S, P>;
|
|
110
|
+
/** Idempotency over a memory store: `true` for the defaults, or the options. Off by default. */
|
|
111
|
+
idempotency?: I & LambderMockSurplusKeys<I, LambderMockIdempotencyOptions<S>>;
|
|
112
|
+
/**
|
|
113
|
+
* The host the runtime's cookies belong to: what signIn plants them
|
|
114
|
+
* under, what the direct transport's jar scopes them by, what the MSW
|
|
115
|
+
* adapter's jar scopes them by, and what a transport request naming no
|
|
116
|
+
* siteHost is read as arriving at. Default: the page's own host, or
|
|
117
|
+
* "localhost" outside a browser.
|
|
118
|
+
*
|
|
119
|
+
* One host per app, because a jar checks a cookie's scope the way a
|
|
120
|
+
* browser does: planted at "localhost" and read back on
|
|
121
|
+
* "transit.localhost:5173", the session cookie is simply not sent, and
|
|
122
|
+
* every session call in a browser served from anything but plain
|
|
123
|
+
* localhost answered sessionExpired.
|
|
124
|
+
*/
|
|
125
|
+
cookieHost?: string;
|
|
126
|
+
/** Ceiling on what a compressed request payload may restore to. Default: 20,000,000. */
|
|
127
|
+
maxRequestPayloadBytes?: number;
|
|
128
|
+
/**
|
|
129
|
+
* The client IP a transport request carrying none is read as. Default:
|
|
130
|
+
* the loopback address. A transport may name its own, which is how a test
|
|
131
|
+
* drives a `per: "ip"` rate limit from two clients.
|
|
132
|
+
*/
|
|
133
|
+
defaultClientIp?: string;
|
|
134
|
+
/** How many completed calls `calls` keeps. Default: 200. */
|
|
135
|
+
callLogSize?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Answer a handler that threw with the message it threw, rather than the
|
|
138
|
+
* server's "Internal server error." Default: true, because a mock runtime
|
|
139
|
+
* is a development tool and the thrown message is the thing worth seeing.
|
|
140
|
+
* Set false to get the production shape.
|
|
141
|
+
*/
|
|
142
|
+
revealHandlerErrors?: boolean;
|
|
143
|
+
/** Called at the end of reset(), so the app can rewind its own data. */
|
|
144
|
+
onReset?: () => void;
|
|
145
|
+
};
|
|
146
|
+
/** How the mock transport carries cookies: a fresh memory jar (default), a jar of yours, the memory jar mirrored into document.cookie, or none. */
|
|
147
|
+
export type LambderMockTransportOptions = {
|
|
148
|
+
cookies?: LambderCookieJar | "memory" | "document" | false;
|
|
149
|
+
/** The client IP its calls arrive from; defaults to the app's defaultClientIp. One transport is one client. */
|
|
150
|
+
clientIp?: string;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* The mock's transport with the jar it carries hanging off it: an ordinary
|
|
154
|
+
* LambderApiTransport wherever one is expected, and reachable where a test
|
|
155
|
+
* needs the jar transport() built for itself (to read a cookie, or to clear
|
|
156
|
+
* it). Null when the transport carries no cookies.
|
|
157
|
+
*/
|
|
158
|
+
export type LambderMockTransport = LambderApiTransport & {
|
|
159
|
+
readonly cookieJar: LambderCookieJar | null;
|
|
160
|
+
};
|
|
161
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What a mock runtime is configured with, and the shapes of what a mock
|
|
3
|
+
* transport takes.
|
|
4
|
+
*
|
|
5
|
+
* Everything create() takes lives here, beside the rules that decide which
|
|
6
|
+
* keys it accepts (the guards option and the surplus-key checks under it),
|
|
7
|
+
* exactly as core/LambderCreateOptions.ts holds the server's.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LambderMockEntry, LambderMockOverride, LambderMockRestEntry } from "./LambderMockTypes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which mock answers an endpoint: the registered entries, and the overrides
|
|
4
|
+
* standing over them.
|
|
5
|
+
*
|
|
6
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
7
|
+
* touches; it meets the rest of the runtime at one call, the lookup a request
|
|
8
|
+
* makes. The app keeps the whole caller-facing surface (register,
|
|
9
|
+
* registerPartial, override, restoreOverrides, registeredNames) as
|
|
10
|
+
* delegations, because that surface is contract-typed and the checks that
|
|
11
|
+
* make it safe are compile-time; what lives here is the bookkeeping.
|
|
12
|
+
*
|
|
13
|
+
* Generic over the contract only so the entries keep their type through the
|
|
14
|
+
* map; the registry itself never reads one.
|
|
15
|
+
*/
|
|
16
|
+
export declare class LambderMockEntryRegistry<C> {
|
|
17
|
+
private readonly entries;
|
|
18
|
+
/**
|
|
19
|
+
* The overrides standing over one endpoint, innermost last. A stack
|
|
20
|
+
* rather than one slot because overrides nest: an override a describe
|
|
21
|
+
* scopes and one an it scopes are both live, and restoring the inner one
|
|
22
|
+
* has to uncover the outer rather than the registry.
|
|
23
|
+
*/
|
|
24
|
+
private readonly overrideStacks;
|
|
25
|
+
/** The reason register() was given a rest entry with; null while it was given none. */
|
|
26
|
+
private restReason;
|
|
27
|
+
/**
|
|
28
|
+
* What a call to an endpoint no slice registered is refused with, or null
|
|
29
|
+
* when no rest entry was registered. A registration like any other, so
|
|
30
|
+
* reset() keeps it.
|
|
31
|
+
*/
|
|
32
|
+
get restNotMockedReason(): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Adds every entry of every slice, and the rest entry where one is among
|
|
35
|
+
* them. Slices are staged and committed together, so a slice that fails a
|
|
36
|
+
* check leaves nothing behind: a caller that catches the error and retries
|
|
37
|
+
* sees the problem it is fixing rather than a duplicate-name error from
|
|
38
|
+
* its own first attempt. The rest entry is staged with them, for the same
|
|
39
|
+
* reason.
|
|
40
|
+
*/
|
|
41
|
+
addSlices(slices: readonly (Record<string, LambderMockEntry<C, any>> | LambderMockRestEntry)[]): void;
|
|
42
|
+
/** The registered entry for a name, before any override; null when there is none. */
|
|
43
|
+
registered(apiName: string): LambderMockEntry<C, any> | null;
|
|
44
|
+
/** Stacks an override over a registered entry and hands back its removal. */
|
|
45
|
+
pushOverride(name: string, entry: LambderMockEntry<C, any>): LambderMockOverride;
|
|
46
|
+
/** Puts every overridden handler back, however deeply they were stacked. */
|
|
47
|
+
restoreOverrides(): void;
|
|
48
|
+
/** The registered endpoint names. */
|
|
49
|
+
get names(): string[];
|
|
50
|
+
/** What answers this call: the innermost override, else the registered entry. */
|
|
51
|
+
entryFor(apiName: string): LambderMockEntry<C, any> | null;
|
|
52
|
+
}
|