lambder 6.0.2 → 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 +21 -19
  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,99 @@
1
+ import { type LambderApiRequest } from "../api/LambderApiRequest.js";
2
+ import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
3
+ import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
4
+ /**
5
+ * The parts of the msw module the adapter uses: `import * as msw from "msw"`.
6
+ *
7
+ * Written so the real package satisfies it, which is the whole point of a
8
+ * structural declaration and was not true of the previous one. msw's resolver
9
+ * answers a Response, or `undefined` to hand the request back (its
10
+ * AsyncResponseResolverReturnType), and a resolver declared to return
11
+ * `Promise<unknown>` is not assignable to that, so `http.post` did not fit
12
+ * here and the handler this returned did not fit `setupWorker`. Both errors
13
+ * landed on the documented five-line wiring.
14
+ */
15
+ export type LambderMswModule = {
16
+ http: {
17
+ post: (path: string, resolver: (info: {
18
+ request: Request;
19
+ }) => Promise<Response | undefined>) => unknown;
20
+ };
21
+ HttpResponse: {
22
+ new (body?: BodyInit | null, init?: ResponseInit): Response;
23
+ error(): Response;
24
+ };
25
+ };
26
+ /** What the adapter needs from the mock app: a parsed request in, an answer out, and the runtime's own bookkeeping. */
27
+ export type LambderMockMswTarget = {
28
+ handleRequest(request: LambderApiRequest): Promise<LambderApiAnswer>;
29
+ /**
30
+ * Whether this name would be answered from the registry, overrides and a
31
+ * registered rest entry included; asked once per request, so not a list to
32
+ * scan.
33
+ */
34
+ hasRegisteredEntry(apiName: string): boolean;
35
+ /** Records a request this adapter handed on rather than answering. */
36
+ notePassthrough(request: LambderApiRequest): void;
37
+ /** Mirrors an answer's readable cookies into document.cookie, and remembers them for reset(). */
38
+ mirrorCookiesIntoDocument(setCookies: readonly string[]): void;
39
+ /** Takes the jar this adapter built as the runtime's own, so reset() empties it too. */
40
+ adoptCookieJar(jar: LambderCookieJar): void;
41
+ /** The IP a call carrying none is read as arriving from, so this adapter and the direct transport report one address. */
42
+ readonly defaultClientIp: string;
43
+ /**
44
+ * The host the runtime's cookies belong to, which this adapter's jar is
45
+ * scoped by. The runtime's value, not the request URL's: signIn plants at
46
+ * the app's cookieHost and the direct transport's jar sends from there, so
47
+ * an adapter scoping by whatever host the page is served from held the
48
+ * session cookies at a host it never sent them to, and every session call
49
+ * behind the worker answered sessionExpired with a full jar.
50
+ */
51
+ readonly cookieHost: string;
52
+ };
53
+ /**
54
+ * ONE MSW request handler for the whole API path, over the mock app: the
55
+ * opt-in that makes mocked calls appear in the browser's network panel as
56
+ * genuine requests, with real method, status, timing and bodies. The request
57
+ * is read the way the server reads it, its headers and Cookie header
58
+ * included.
59
+ *
60
+ * Session cookies are held in a jar here rather than by the browser, because
61
+ * the browser will not hold them: a response a service worker synthesizes
62
+ * never reaches the cookie store, and MSW's own jar comma-joins the
63
+ * Set-Cookie headers before parsing them, which loses every cookie after the
64
+ * first. So the answer's cookies go into the jar, the next request carries
65
+ * them back, and the ones a page's scripts may see are mirrored into
66
+ * document.cookie. The Set-Cookie headers still travel on the response, where
67
+ * the network panel shows them.
68
+ *
69
+ * Lambder never depends on msw: the app installs it and passes the module
70
+ * in. An injected network failure answers MSW's network error. A POST whose
71
+ * body is not an API envelope is left to other handlers.
72
+ *
73
+ * Generic over the module so the handler keeps msw's own handler type, which
74
+ * is what `setupWorker(...)` and `setupServer(...)` take. Returning `unknown`
75
+ * made the documented wiring an error at the consumer.
76
+ */
77
+ export declare const lambderMockMswHandler: <M extends LambderMswModule>(mockApp: LambderMockMswTarget, options: {
78
+ msw: M;
79
+ apiPath: string;
80
+ cookieJar?: LambderCookieJar;
81
+ /**
82
+ * What happens to a call the runtime has no entry for. "refuse"
83
+ * (the default) answers the apiNotFound refusal, which is what an
84
+ * exhaustive `register` is for. "passthrough" leaves the request to
85
+ * MSW's other handlers and, failing those, to the network: the shape
86
+ * a partially mocked app runs in while its remaining endpoints still
87
+ * come from a real backend.
88
+ *
89
+ * This and `mockApp.restNotMocked(reason)` are the two answers to the
90
+ * same question, and the rest entry wins: it leaves the runtime with
91
+ * an entry for every name, so nothing is ever unmocked here and a call
92
+ * that would have gone to the network is answered notMocked instead.
93
+ * Pick the rest entry for an app with no backend to reach, and this
94
+ * for one whose remaining endpoints are served by a real one.
95
+ */
96
+ onUnmocked?: "refuse" | "passthrough";
97
+ /** The client IP its calls are read as arriving from. Default: the runtime's own defaultClientIp. */
98
+ clientIp?: string;
99
+ }) => ReturnType<M["http"]["post"]>;
@@ -0,0 +1,126 @@
1
+ import { LambderMockTransportError } from "./LambderMockFailureInjector.js";
2
+ import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
3
+ import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
4
+ import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
5
+ import { normalizeClientIp } from "../shared/util/LambderClientIp.js";
6
+ /**
7
+ * ONE MSW request handler for the whole API path, over the mock app: the
8
+ * opt-in that makes mocked calls appear in the browser's network panel as
9
+ * genuine requests, with real method, status, timing and bodies. The request
10
+ * is read the way the server reads it, its headers and Cookie header
11
+ * included.
12
+ *
13
+ * Session cookies are held in a jar here rather than by the browser, because
14
+ * the browser will not hold them: a response a service worker synthesizes
15
+ * never reaches the cookie store, and MSW's own jar comma-joins the
16
+ * Set-Cookie headers before parsing them, which loses every cookie after the
17
+ * first. So the answer's cookies go into the jar, the next request carries
18
+ * them back, and the ones a page's scripts may see are mirrored into
19
+ * document.cookie. The Set-Cookie headers still travel on the response, where
20
+ * the network panel shows them.
21
+ *
22
+ * Lambder never depends on msw: the app installs it and passes the module
23
+ * in. An injected network failure answers MSW's network error. A POST whose
24
+ * body is not an API envelope is left to other handlers.
25
+ *
26
+ * Generic over the module so the handler keeps msw's own handler type, which
27
+ * is what `setupWorker(...)` and `setupServer(...)` take. Returning `unknown`
28
+ * made the documented wiring an error at the consumer.
29
+ */
30
+ export const lambderMockMswHandler = (mockApp, options) => {
31
+ const { msw, apiPath } = options;
32
+ if (!msw?.http || !msw?.HttpResponse) {
33
+ throw new Error('lambderMockMswHandler requires the msw module: lambderMockMswHandler(mockApp, { apiPath, msw: await import("msw") }). Install it with: npm install msw --save-dev');
34
+ }
35
+ const jar = options.cookieJar ?? new LambderCookieJar();
36
+ // A jar the app passed stays the app's; the one built here is the
37
+ // runtime's, so its reset() empties it along with the sessions those
38
+ // cookies name.
39
+ if (!options.cookieJar)
40
+ mockApp.adoptCookieJar(jar);
41
+ // The runtime's default, not a second one of this adapter's own: an app
42
+ // that set defaultClientIp saw its address through the direct transport
43
+ // and 127.0.0.1 through the service worker, so a per-IP rate limit counted
44
+ // two clients where there was one and the two adapters disagreed about
45
+ // what ctx.request.ip is.
46
+ // Normalized the way every other adapter's is, so one address is one
47
+ // counter under a `per: "ip"` limit however it was spelled.
48
+ const clientIp = normalizeClientIp(options.clientIp ?? mockApp.defaultClientIp);
49
+ const handler = msw.http.post(apiPath, async ({ request }) => {
50
+ let post;
51
+ try {
52
+ post = await request.clone().json();
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ // Through the one header map every adapter builds, which is built on
58
+ // Object.create(null): a header literally named __proto__ was dropped
59
+ // here and landed as an own key on the server, and headers["toString"]
60
+ // handed a guard an inherited function where every other adapter gives
61
+ // undefined.
62
+ const headers = lowercaseHeaderNames(Object.fromEntries(request.headers));
63
+ const url = new URL(request.url);
64
+ // Only the cookies whose scope covers this call, as a browser would
65
+ // send: reading the whole jar sent one host's session to another as
66
+ // soon as a jar was shared across hosts, and read cookies the request
67
+ // path was never in scope for. The host is the runtime's own, the path
68
+ // the one this call is going to. The jar's copies come first: where a
69
+ // name is in both, the jar holds what this runtime last set and the
70
+ // document's copy is the mirror of it, so the jar is the one to
71
+ // believe.
72
+ const cookieScope = { host: mockApp.cookieHost, path: url.pathname };
73
+ const cookies = cookieValuesByName(jar.cookiePairs(cookieScope));
74
+ // Pair by pair, so a name the document holds at two scopes keeps both
75
+ // values and the session controller can weigh them, as it does on
76
+ // the server.
77
+ for (const [name, values] of Object.entries(cookieValuesByName((request.headers.get("cookie") ?? "").split(";")))) {
78
+ for (const value of values) {
79
+ if (!cookies[name]?.includes(value))
80
+ (cookies[name] ??= []).push(value);
81
+ }
82
+ }
83
+ const parsed = readApiEnvelope(post, {
84
+ headers, cookies, ip: clientIp, host: url.host, signal: request.signal,
85
+ });
86
+ if (!parsed)
87
+ return undefined;
88
+ // Returning undefined hands the request back to MSW, which tries its
89
+ // other handlers and then the network. Noted on the runtime first:
90
+ // a passthrough that leaves no event and no call-log row is a
91
+ // mistyped endpoint name reaching the real backend in silence.
92
+ if (options.onUnmocked === "passthrough" && !mockApp.hasRegisteredEntry(parsed.apiName)) {
93
+ mockApp.notePassthrough(parsed);
94
+ return undefined;
95
+ }
96
+ let answer;
97
+ try {
98
+ answer = await mockApp.handleRequest(parsed);
99
+ }
100
+ catch (err) {
101
+ if (err instanceof LambderMockTransportError)
102
+ return msw.HttpResponse.error();
103
+ throw err;
104
+ }
105
+ const setCookies = getAnswerHeader(answer.headers, "Set-Cookie") ?? [];
106
+ jar.storeSetCookies(setCookies, cookieScope);
107
+ // The cookies a page's own scripts may see are mirrored into
108
+ // document.cookie, so it reads the same in mocked development as it
109
+ // does against the real backend. Not decoration: the browser caller
110
+ // reads the CSRF token from there and posts it on the envelope, so
111
+ // without this every session call fails its CSRF check. Through the
112
+ // runtime, which is where the one mirror implementation lives and
113
+ // where what was planted is remembered for reset().
114
+ mockApp.mirrorCookiesIntoDocument(setCookies);
115
+ const responseHeaders = new Headers();
116
+ for (const [key, values] of Object.entries(answer.headers)) {
117
+ for (const value of values)
118
+ responseHeaders.append(key, value);
119
+ }
120
+ return new msw.HttpResponse(answer.body, { status: answer.statusCode, headers: responseHeaders });
121
+ });
122
+ // The call above is resolved against the constraint, which says `unknown`;
123
+ // what the module actually hands back is its own handler type, and naming
124
+ // it is the whole reason this function is generic.
125
+ return handler;
126
+ };
package/dist/mock.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Mock entry point (`import ... from "lambder/mock"`).
3
+ *
4
+ * The mock runtime: the API core over memory stores, serving a typed
5
+ * contract from mock handlers in development and in tests, in a browser or
6
+ * in Node. Browser-safe like `lambder/client`, and never part of a
7
+ * production bundle by construction: nothing else imports it.
8
+ */
9
+ export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
10
+ export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
11
+ export type { LambderMockAppOptions, LambderMockSessionsOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./mock/LambderMockCreateOptions.js";
12
+ export type { LambderMockCallContext, LambderMockSessionCallContext, LambderMockContext, LambderMockGuards, LambderMockHandler, LambderMockEntry, LambderMockEntryOptions, LambderMockEntryInput, LambderMockSlice, LambderMockRestEntry, LambderMockRegistryCheck, LambderMockMissingNames, LambderMockStrayNames, LambderMockDuplicateNames, LambderMockPublicNames, LambderMockSessionNames, LambderMockLatency, LambderMockFailure, LambderMockFailureReason, LambderMockOutcome, LambderMockCallEvent, LambderMockRequestEvent, LambderMockResponseEvent, LambderMockCallRecord, LambderMockListener, LambderMockRateLimitPolicies, LambderMockInputOf, LambderMockOutputOf, LambderMockOverride, } from "./mock/LambderMockTypes.js";
13
+ export { lambderMockConsoleLogger } from "./mock/lambderMockConsoleLogger.js";
14
+ export type { LambderMockConsoleLoggerOptions } from "./mock/lambderMockConsoleLogger.js";
15
+ export { lambderMockMswHandler } from "./mock/lambderMockMswHandler.js";
16
+ export type { LambderMswModule, LambderMockMswTarget } from "./mock/lambderMockMswHandler.js";
17
+ export { lambderMockInvokeTransport } from "./mock/lambderMockInvokeTransport.js";
18
+ export type { LambderMockInvokeEvent, LambderMockInvokeResult } from "./mock/lambderMockInvokeTransport.js";
19
+ export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
20
+ export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
21
+ export type { LambderApiTransport, LambderApiTransportRequest } from "./shared/transport/LambderApiTransport.js";
22
+ export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
23
+ export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
24
+ export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
25
+ export { LambderWebCrypto, LambderPlainSessionCrypto } from "./session/LambderSessionCrypto.js";
26
+ export type { LambderSessionCrypto } from "./session/LambderSessionCrypto.js";
27
+ export { LambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
28
+ export type { LambderRefusalMessage } from "./shared/wire/LambderApiRefusal.js";
29
+ export type { LambderApiRequest } from "./api/LambderApiRequest.js";
30
+ export type { LambderApiAnswer } from "./api/LambderApiAnswer.js";
31
+ export type { LambderSessionRecord } from "./shared/contracts/LambderSessionStore.js";
32
+ export type { LambderCreatedSession } from "./session/LambderSessionManager.js";
33
+ export type { default as LambderSessionManager } from "./session/LambderSessionManager.js";
34
+ export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
package/dist/mock.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Mock entry point (`import ... from "lambder/mock"`).
3
+ *
4
+ * The mock runtime: the API core over memory stores, serving a typed
5
+ * contract from mock handlers in development and in tests, in a browser or
6
+ * in Node. Browser-safe like `lambder/client`, and never part of a
7
+ * production bundle by construction: nothing else imports it.
8
+ */
9
+ export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
10
+ // The four collaborators behind LambderMockApp (the entry registry, the call
11
+ // recorder, the failure injector, the browser cookies) are deliberately not
12
+ // exported: the runtime is reached through the app, and its surface did not
13
+ // change when they moved out of it. Only the error a transport rejects with is
14
+ // public, as before.
15
+ export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
16
+ export { lambderMockConsoleLogger } from "./mock/lambderMockConsoleLogger.js";
17
+ export { lambderMockMswHandler } from "./mock/lambderMockMswHandler.js";
18
+ export { lambderMockInvokeTransport } from "./mock/lambderMockInvokeTransport.js";
19
+ // What a mock setup reaches for beside the app: the stores it runs on, the
20
+ // jar its transport carries, and the refusal a handler says no with.
21
+ export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
22
+ export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
23
+ export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
24
+ export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
25
+ export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
26
+ export { LambderWebCrypto, LambderPlainSessionCrypto } from "./session/LambderSessionCrypto.js";
27
+ export { LambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
@@ -1,7 +1,19 @@
1
- import { LambderRenderContext, LambderSessionRenderContext } from "../core/LambderContext.js";
2
- import { type LambderCookieOptions } from "../core/LambderCookie.js";
1
+ import { type LambderCookieOptions } from "../shared/wire/LambderCookie.js";
2
+ import type { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
3
+ import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
3
4
  import type LambderSessionManager from "./LambderSessionManager.js";
4
- import { type LambderSessionContext } from "./LambderSessionManager.js";
5
+ import type { LambderCreatedSession } from "./LambderSessionManager.js";
6
+ /**
7
+ * The part of a call this controller touches: the session it reads and
8
+ * writes, and the headers it puts Set-Cookie on. Declaring it here is what
9
+ * keeps the session layer under the API core rather than beside it: the
10
+ * pipeline and both adapters pass their own full call context, which is
11
+ * structurally this plus the fields the controller never reads.
12
+ */
13
+ export type LambderSessionCallSurface<TSessionData = any> = {
14
+ session: LambderSessionRecord<TSessionData> | null;
15
+ responseHeaders: LambderAnswerHeaders;
16
+ };
5
17
  /**
6
18
  * Scope of the session cookies. `domain` is e.g. ".example.com" to share
7
19
  * sessions across subdomains, or a function of the request hostname when
@@ -9,49 +21,206 @@ import { type LambderSessionContext } from "./LambderSessionManager.js";
9
21
  * host-only cookie). Changing `domain` or `path` on a live deployment is a
10
22
  * migration: browsers keep the cookie under the old scope beside the new
11
23
  * one, and both arrive on every request. fetchSession tolerates that by
12
- * trying every copy and evicting the stale host-only twin; a copy at a
13
- * parent domain this host cannot name outlives its own Expires.
24
+ * scanning every copy for the one live session and evicting the stale
25
+ * host-only twin; a copy at a parent domain this host cannot name outlives
26
+ * its own Expires.
14
27
  */
15
28
  export type LambderSessionCookieOptions = Pick<LambderCookieOptions, "domain" | "path" | "sameSite" | "secure">;
16
- export default class LambderSessionController<TSessionData = any> {
17
- lambderSessionManager: LambderSessionManager;
18
- sessionTokenCookieKey: string;
19
- sessionCsrfCookieKey: string;
29
+ /**
30
+ * What the controller reads off the request: the host (cookie domain
31
+ * resolution), every value of every cookie, and the CSRF token the caller
32
+ * posted, or null when the request is not an API call (a route), in which
33
+ * case no CSRF check applies.
34
+ */
35
+ export type LambderSessionRequestInfo = {
36
+ host: string;
37
+ cookies: Record<string, string[]>;
38
+ csrfToken: string | null;
39
+ };
40
+ export type LambderSessionControllerOptions<TSessionData> = {
41
+ manager: LambderSessionManager<TSessionData>;
42
+ /**
43
+ * Name of the session cookie. Required rather than defaulted here: the
44
+ * defaults live in shared/wire/LambderSessionCookieNames.ts and are applied
45
+ * once, where the app's session options are read, so a second copy of
46
+ * them in this constructor would be a second place for them to drift and
47
+ * would let a controller read a cookie name the app never writes.
48
+ */
49
+ tokenCookieKey: string;
50
+ /** Name of the CSRF cookie, required on the same terms as tokenCookieKey. */
51
+ csrfCookieKey: string;
52
+ cookieOptions?: LambderSessionCookieOptions;
53
+ /** The call context the session is read onto and whose responseHeaders receive the cookies. */
54
+ ctx: LambderSessionCallSurface<TSessionData>;
55
+ request: LambderSessionRequestInfo;
56
+ };
57
+ /**
58
+ * No session for this request: the cookies named none, or the single session
59
+ * they named did not pair with the posted CSRF token.
60
+ *
61
+ * Typed rather than a bare Error because fetchSessionIfExists has to tell
62
+ * "there is no session here" apart from "something broke". Everything else,
63
+ * a TypeError from a custom store, a bug in an app's dataRefresh callback,
64
+ * propagates and becomes a crash: answering sessionExpired for a defect makes
65
+ * the client clear its cookies and turns somebody's bug into a logout.
66
+ */
67
+ export declare class LambderSessionNotFoundError extends Error {
68
+ constructor(message?: string);
69
+ }
70
+ /**
71
+ * The request's session cookies cannot be resolved to one session, so none of
72
+ * them is used and every scope this host can write is cleared. Also a "no
73
+ * session" answer to the caller, and deliberately a different type: this one
74
+ * carries the clearing Set-Cookie headers that heal the state, and it is the
75
+ * one worth finding in a log.
76
+ */
77
+ export declare class LambderSessionAmbiguousError extends Error {
78
+ constructor(message?: string);
79
+ }
80
+ /**
81
+ * A `__Host-` cookie is the browser's own answer to a sibling subdomain
82
+ * planting a session cookie at a parent domain: it refuses one that carries a
83
+ * Domain, so no other host can write it. `__Secure-` is the weaker sibling,
84
+ * accepted only on a Secure cookie. Both protections fail silently, though: a
85
+ * browser handed a prefixed name with an attribute the prefix forbids simply
86
+ * discards the cookie, and the app looks like it has no sessions at all
87
+ * rather than like it is misconfigured. So the combinations are rejected at
88
+ * creation instead.
89
+ *
90
+ * Session policy, so it lives beside the controller that writes the cookies
91
+ * rather than in the pipeline that happens to call it.
92
+ */
93
+ export declare const assertSessionCookiePrefixes: (sessions: {
94
+ tokenCookieKey: string;
95
+ csrfCookieKey: string;
20
96
  cookieOptions: LambderSessionCookieOptions;
21
- ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>;
22
- constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, cookieOptions, ctx, }: {
23
- lambderSessionManager: LambderSessionManager;
24
- sessionTokenCookieKey: string;
25
- sessionCsrfCookieKey: string;
26
- cookieOptions?: LambderSessionCookieOptions;
27
- ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>;
28
- });
97
+ }) => void;
98
+ /**
99
+ * Sessions as one request sees them: reads the session the request's
100
+ * cookies name onto the context, and writes the cookies a created,
101
+ * rotated or ended session needs into the context's response headers.
102
+ * Server handlers reach it through lambder.getSessionController(ctx); mock
103
+ * handlers through ctx.sessions. It works on the call context and the
104
+ * request info alone, so it is one class for both.
105
+ */
106
+ export default class LambderSessionController<TSessionData = any> {
107
+ readonly manager: LambderSessionManager<TSessionData>;
108
+ readonly tokenCookieKey: string;
109
+ readonly csrfCookieKey: string;
110
+ readonly cookieOptions: LambderSessionCookieOptions;
111
+ private readonly ctx;
112
+ private readonly request;
113
+ constructor({ manager, tokenCookieKey, csrfCookieKey, cookieOptions, ctx, request }: LambderSessionControllerOptions<TSessionData>);
29
114
  /** The configured scope with the domain resolved for this request, or the host-only scope. */
30
115
  private cookieScope;
31
- /** Raw secrets exist only on the LambderCreatedSession result and in these cookies; the record stores hashes. */
116
+ /**
117
+ * Both cookies at one expiry, with the raw secrets. They exist only on the
118
+ * LambderCreatedSession result, in these cookies and in the request that
119
+ * carried them back; the record stores hashes. `csrfToken` is null where
120
+ * the raw CSRF value is not known to this request, in which case only the
121
+ * session cookie is written: writing a CSRF cookie whose value does not
122
+ * pair with the session would break the very session it is refreshing.
123
+ */
124
+ private writeSessionCookies;
32
125
  private setSessionCookies;
126
+ /**
127
+ * The deleting pair for one scope. Written once because a deletion only
128
+ * reaches a cookie carrying the same Domain and Path, so the pair is
129
+ * emitted per scope and the two callers differ in nothing but which
130
+ * scopes they walk.
131
+ */
132
+ private addClearCookiePair;
33
133
  private clearSessionCookies;
34
134
  /**
35
- * Every well-formed value the request carried under the session cookie
36
- * name. More than one means the browser holds the cookie at several
37
- * scopes, and the order says nothing about which copy is current.
135
+ * Every Domain this host is allowed to write the session cookies at: the
136
+ * host-only scope, the configured one, and each parent domain of the
137
+ * request host. A deletion matches only a cookie carrying the same
138
+ * Domain, so evicting a copy the app itself never set needs all of them.
139
+ * A browser ignores a Domain it will not accept, which is why a suffix
140
+ * the registry owns can be offered without checking a public-suffix list.
141
+ */
142
+ private cookieClearDomains;
143
+ /**
144
+ * Clears the session cookies at every scope this host can reach, rather
145
+ * than at the one the app configured. Used when a request carries more
146
+ * than one live session: the copy that has to go may sit at a parent
147
+ * domain a sibling host planted it at, and clearing the configured scope
148
+ * alone would evict this visitor's own cookie and leave the planted one
149
+ * as the only survivor, which completes the takeover instead of stopping
150
+ * it.
151
+ */
152
+ private clearSessionCookiesEverywhere;
153
+ /**
154
+ * Refuses a request whose session cookies cannot be resolved to one
155
+ * session, clearing every scope this host can write. Clearing only the
156
+ * configured scope would be worse than picking one: a deletion matches
157
+ * only a cookie carrying the same Domain, so it would evict the visitor's
158
+ * own copy and leave a planted one as the sole survivor.
159
+ *
160
+ * Path is the one dimension this cannot sweep: the request info carries
161
+ * no path, and a deletion matches only a cookie at the same Path, so a
162
+ * copy planted at a deeper path stays out of reach. A `__Host-` cookie
163
+ * name is the structural answer, since the prefix forbids Domain and
164
+ * pins Path to "/"; see docs/sessions.md.
38
165
  */
39
- private sessionTokenCandidates;
166
+ private refuseAmbiguousSession;
167
+ /**
168
+ * Both session cookie names read in one pass: every well-formed value
169
+ * under the token name, and every value under the CSRF name.
170
+ *
171
+ * The CSRF cookie is counted here rather than looked at only when a token
172
+ * is checked against it, because it is plantable exactly like the session
173
+ * cookie and the browser picks between copies without telling anyone: the
174
+ * client reads its CSRF token with js-cookie's Cookies.get, which returns
175
+ * the FIRST copy in document.cookie, and a browser orders a longer Path
176
+ * first. So a sibling host that plants one CSRF cookie at a parent domain
177
+ * with a deeper Path decides which token every call posts, and the count
178
+ * is the only thing that shows it.
179
+ */
180
+ private scanSessionCookies;
181
+ /** An API call must also carry a CSRF token; a route only needs the cookie. */
40
182
  private areRequestSessionTokensValid;
41
- createSession(sessionKey: string, data?: TSessionData, ttlInSeconds?: number): Promise<LambderSessionContext<TSessionData>>;
42
- regenerateSession(): Promise<LambderSessionContext<TSessionData>>;
43
- fetchSession(): Promise<LambderSessionContext<TSessionData>>;
44
- fetchSessionIfExists(): Promise<LambderSessionContext<TSessionData> | null>;
45
- /** Checks the record against a presented token (the request's first session cookie by default) and, on API calls, the posted CSRF token. */
46
- isSessionValid(session: any, sessionToken?: string | undefined): boolean;
47
- updateSessionData(newData: any): Promise<LambderSessionContext>;
183
+ createSession(sessionKey: string, data?: TSessionData, ttlInSeconds?: number): Promise<LambderSessionRecord<TSessionData>>;
184
+ /**
185
+ * createSession, handing back the raw tokens beside the session: what a
186
+ * test or a mock runtime needs to plant the cookies somewhere else (a
187
+ * cookie jar) than this call's response.
188
+ */
189
+ issueSession(sessionKey: string, data?: TSessionData, ttlInSeconds?: number): Promise<LambderCreatedSession<TSessionData>>;
190
+ regenerateSession(): Promise<LambderSessionRecord<TSessionData>>;
191
+ /**
192
+ * regenerateSession, handing back the raw tokens beside the session, the
193
+ * way issueSession does for a new one. Rotating the session mints a new
194
+ * CSRF token, and a client that holds its token rather than reading
195
+ * document.cookie (a native app, an invoke caller) needs the new one to
196
+ * keep calling.
197
+ */
198
+ reissueSession(): Promise<LambderCreatedSession<TSessionData>>;
199
+ fetchSession(): Promise<LambderSessionRecord<TSessionData>>;
200
+ /**
201
+ * Re-issues both cookies at this session's expiry: after a sliding write
202
+ * moved it, and beside the host-only eviction above, which would
203
+ * otherwise delete a cookie without replacing it.
204
+ *
205
+ * The raw CSRF value is the posted one on an API call, which the pairing
206
+ * check above has just matched against this session. A route posts none,
207
+ * so the single arriving CSRF cookie stands in, and only once it is known
208
+ * to pair: re-issuing an unpaired value would overwrite this visitor's
209
+ * real CSRF cookie with a planted one, at the app's own scope, which is
210
+ * the takeover the scan exists to prevent. Where neither is available the
211
+ * session cookie slides alone, which is the half that decides whether the
212
+ * session survives.
213
+ */
214
+ private slideSessionCookies;
215
+ fetchSessionIfExists(): Promise<LambderSessionRecord<TSessionData> | null>;
216
+ updateSessionData(newData: TSessionData): Promise<LambderSessionRecord<TSessionData>>;
48
217
  /**
49
218
  * Force-runs the dataRefresh callback now (see the session option of create) and
50
219
  * persists the result onto the current session. Returns the updated
51
220
  * session, or null when the callback ended it: the record is deleted and
52
221
  * the session cookies are cleared.
53
222
  */
54
- refreshSessionData(): Promise<LambderSessionContext<TSessionData> | null>;
223
+ refreshSessionData(): Promise<LambderSessionRecord<TSessionData> | null>;
55
224
  /**
56
225
  * Deletes every session of the given sessionKey (e.g. a user id): "log
57
226
  * this subject out everywhere". Unlike endSessionAll it needs no fetched