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.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +26 -24
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /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
+ }