lambder 14.1.1 → 15.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +14 -17
  3. package/dist/api/LambderApiDeclarations.d.ts +11 -5
  4. package/dist/api/LambderApiDefinition.d.ts +2 -1
  5. package/dist/api/LambderApiEnvelope.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.js +8 -2
  7. package/dist/api/LambderApiGuards.d.ts +44 -5
  8. package/dist/api/LambderApiGuards.js +27 -0
  9. package/dist/api/LambderApiIdempotency.d.ts +14 -7
  10. package/dist/api/LambderApiIdempotency.js +39 -27
  11. package/dist/api/LambderApiPipeline.d.ts +25 -11
  12. package/dist/api/LambderApiPipeline.js +34 -17
  13. package/dist/api/LambderApiRateLimits.d.ts +28 -21
  14. package/dist/api/LambderApiRateLimits.js +55 -35
  15. package/dist/api/LambderApiRefusals.d.ts +10 -4
  16. package/dist/api/LambderApiRefusals.js +31 -5
  17. package/dist/api/LambderApiRequest.d.ts +21 -1
  18. package/dist/api/LambderApiRequest.js +31 -2
  19. package/dist/build/checkApiRefusals.d.ts +8 -3
  20. package/dist/build/checkApiRefusals.js +80 -29
  21. package/dist/build/contractPrinterProcess.d.ts +11 -0
  22. package/dist/build/contractPrinterProcess.js +17 -0
  23. package/dist/build/generateApiFiles.d.ts +26 -5
  24. package/dist/build/generateApiFiles.js +68 -7
  25. package/dist/build/writeApiSchemas.d.ts +109 -0
  26. package/dist/build/writeApiSchemas.js +308 -0
  27. package/dist/build.d.ts +3 -1
  28. package/dist/build.js +2 -0
  29. package/dist/client/LambderCaller.d.ts +25 -11
  30. package/dist/client/LambderCaller.js +57 -49
  31. package/dist/client/LambderUploadRunner.d.ts +55 -27
  32. package/dist/client/LambderUploadRunner.js +89 -34
  33. package/dist/client.d.ts +6 -6
  34. package/dist/client.js +2 -2
  35. package/dist/core/Lambder.d.ts +31 -21
  36. package/dist/core/Lambder.js +79 -23
  37. package/dist/core/LambderCallSummary.d.ts +1 -1
  38. package/dist/core/LambderContext.d.ts +59 -7
  39. package/dist/core/LambderContext.js +31 -15
  40. package/dist/core/LambderCrashHandling.d.ts +5 -0
  41. package/dist/core/LambderCrashHandling.js +11 -1
  42. package/dist/core/LambderCreateOptions.d.ts +28 -17
  43. package/dist/core/LambderCreateOptions.js +14 -7
  44. package/dist/core/LambderResolver.d.ts +3 -2
  45. package/dist/core/LambderResponseBuilder.d.ts +20 -9
  46. package/dist/core/LambderResponseBuilder.js +39 -8
  47. package/dist/core/LambderTemplatingEngine.d.ts +24 -6
  48. package/dist/core/LambderTemplatingEngine.js +23 -3
  49. package/dist/index.d.ts +15 -12
  50. package/dist/index.js +7 -5
  51. package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
  52. package/dist/invoke/LambderInvokeCaller.js +47 -9
  53. package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
  54. package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
  55. package/dist/mock/LambderMockAdapterDoor.js +10 -0
  56. package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
  57. package/dist/mock/LambderMockApiSchemas.js +150 -0
  58. package/dist/mock/LambderMockApp.d.ts +56 -48
  59. package/dist/mock/LambderMockApp.js +107 -62
  60. package/dist/mock/LambderMockBrowserCookies.js +18 -5
  61. package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
  62. package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
  63. package/dist/mock/LambderMockFailureInjector.js +12 -8
  64. package/dist/mock/LambderMockTypes.d.ts +8 -4
  65. package/dist/mock/lambderMockInvokeTransport.js +2 -9
  66. package/dist/mock/lambderMockMswHandler.d.ts +19 -13
  67. package/dist/mock/lambderMockMswHandler.js +25 -22
  68. package/dist/mock.d.ts +1 -0
  69. package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
  70. package/dist/secrets/LambderOneShotSecrets.js +6 -36
  71. package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
  72. package/dist/secrets/LambderPasswordHasher.js +168 -0
  73. package/dist/session/LambderSessionController.d.ts +37 -4
  74. package/dist/session/LambderSessionController.js +36 -6
  75. package/dist/session/LambderSessionManager.d.ts +33 -3
  76. package/dist/session/LambderSessionManager.js +55 -16
  77. package/dist/shared/LambderHtml.d.ts +23 -4
  78. package/dist/shared/LambderHtml.js +17 -5
  79. package/dist/shared/LambderI18n.d.ts +106 -16
  80. package/dist/shared/LambderI18n.js +259 -74
  81. package/dist/shared/contracts/LambderCache.d.ts +14 -1
  82. package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
  83. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  84. package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
  85. package/dist/shared/util/LambderBackoffTimer.js +40 -12
  86. package/dist/shared/util/LambderCallAbort.d.ts +22 -6
  87. package/dist/shared/util/LambderCallAbort.js +37 -9
  88. package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
  89. package/dist/shared/util/LambderErrorReportMark.js +22 -0
  90. package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
  91. package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
  92. package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
  93. package/dist/shared/util/LambderSignedClaims.js +54 -0
  94. package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
  95. package/dist/shared/util/LambderSwappableInstances.js +54 -0
  96. package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
  97. package/dist/shared/util/LambderTestingDoors.js +6 -2
  98. package/dist/shared/util/LambderTextDigest.d.ts +5 -5
  99. package/dist/shared/util/LambderTextDigest.js +5 -5
  100. package/dist/shared/wire/LambderApiContract.d.ts +1 -1
  101. package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
  102. package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
  103. package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
  104. package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
  105. package/dist/shared/wire/LambderApiOutcome.js +1 -1
  106. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
  107. package/dist/shared/wire/LambderApiRefusal.js +3 -3
  108. package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
  109. package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
  110. package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
  111. package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
  112. package/dist/shared/wire/LambderCallOutcome.js +3 -2
  113. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
  114. package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
  115. package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
  116. package/dist/shared/wire/LambderInvokeApiId.js +3 -3
  117. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
  118. package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
  119. package/dist/stores/LambderCacheValues.d.ts +2 -5
  120. package/dist/stores/LambderDdbCache.d.ts +15 -9
  121. package/dist/stores/LambderDdbCache.js +25 -6
  122. package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
  123. package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
  124. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
  125. package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
  126. package/dist/stores/LambderDdbSdk.d.ts +59 -29
  127. package/dist/stores/LambderDdbSdk.js +116 -44
  128. package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
  129. package/dist/stores/LambderDdbSessionStore.js +133 -68
  130. package/dist/stores/LambderMemoryCache.d.ts +2 -2
  131. package/dist/stores/LambderS3FileSource.d.ts +3 -2
  132. package/dist/stores/LambderS3FileSource.js +11 -13
  133. package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
  134. package/dist/stores/LambderS3UploadBucket.js +23 -0
  135. package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
  136. package/dist/stores/LambderStoredText.d.ts +26 -0
  137. package/dist/stores/LambderStoredText.js +25 -0
  138. package/dist/testing/LambderConformanceRunner.d.ts +9 -2
  139. package/dist/testing/LambderConformanceRunner.js +9 -2
  140. package/dist/testing/LambderTestApp.d.ts +65 -7
  141. package/dist/testing/LambderTestApp.js +101 -6
  142. package/dist/testing/LambderTestVisitor.d.ts +43 -12
  143. package/dist/testing/LambderTestVisitor.js +35 -11
  144. package/dist/testing/bootLambdaPackage.d.ts +105 -0
  145. package/dist/testing/bootLambdaPackage.js +265 -0
  146. package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
  147. package/dist/testing/lambdaPackageBootProcess.js +130 -0
  148. package/dist/testing/lambderCacheConformance.d.ts +26 -0
  149. package/dist/testing/lambderCacheConformance.js +287 -0
  150. package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
  151. package/dist/testing/lambderCacheStorageConformance.js +159 -0
  152. package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
  153. package/dist/testing/lambderSessionStoreConformance.js +3 -3
  154. package/dist/testing.d.ts +7 -1
  155. package/dist/testing.js +5 -0
  156. package/package.json +1 -6
  157. package/dist/shared/util/boundKeyField.d.ts +0 -20
  158. package/dist/shared/util/boundKeyField.js +0 -34
@@ -20,6 +20,8 @@ import { LambderMockFailureInjector, LambderMockTransportError } from "./Lambder
20
20
  import { LambderMockCallRecorder } from "./LambderMockCallRecorder.js";
21
21
  import { LambderMockEntryRegistry } from "./LambderMockEntryRegistry.js";
22
22
  import { LambderMockBrowserCookies } from "./LambderMockBrowserCookies.js";
23
+ import { LambderMockApiSchemas } from "./LambderMockApiSchemas.js";
24
+ import { LAMBDER_MOCK_ADAPTER_DOOR } from "./LambderMockAdapterDoor.js";
23
25
  import { LambderMemoryRateLimiter } from "../stores/LambderMemoryRateLimiter.js";
24
26
  import { LambderMemoryIdempotencyStore } from "../stores/LambderMemoryIdempotencyStore.js";
25
27
  import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.js";
@@ -33,6 +35,8 @@ import { DEFAULT_SESSION_CSRF_COOKIE_KEY, DEFAULT_SESSION_TOKEN_COOKIE_KEY } fro
33
35
  const transportCarrying = (transport, jar) => Object.assign(transport, { cookieJar: jar });
34
36
  const DEFAULT_CALL_LOG_SIZE = 200;
35
37
  const DEFAULT_SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
38
+ /** The salt the mock's sessions are keyed with, and so its policy keys' digest, when the app names none. */
39
+ const DEFAULT_MOCK_SESSION_SALT = "lambder-mock";
36
40
  /**
37
41
  * The host the runtime's cookies belong to when the app names none: the page
38
42
  * the mock is running in, or "localhost" where there is no page (a Node test).
@@ -73,6 +77,29 @@ export class LambderMockApp {
73
77
  defaultClientIp;
74
78
  /** The host this runtime's cookies belong to (see the cookieHost option). */
75
79
  cookieHost;
80
+ /**
81
+ * The page's cookie jar: where the browser this runtime serves keeps the
82
+ * cookies no page script can read, the HttpOnly session cookie among
83
+ * them. signIn and signOut plant into it and clear it when they are given
84
+ * no jar, and the MSW adapter carries its calls' cookies in it when it is
85
+ * given none, so the session signIn starts is the one the page's next
86
+ * call through the worker sends. The runtime's own: reset() empties it.
87
+ */
88
+ pageCookieJar = new LambderCookieJar();
89
+ /**
90
+ * What the MSW adapter asks of the runtime and tells it beyond handing it
91
+ * a call, behind a key no entry point exports (see
92
+ * LAMBDER_MOCK_ADAPTER_DOOR): the adapter's bookkeeping, not the app's.
93
+ */
94
+ [LAMBDER_MOCK_ADAPTER_DOOR] = {
95
+ hasRegisteredEntry: (apiName) => this.entryFor(apiName) !== null || this.registry.restNotMockedReason !== null,
96
+ notePassthrough: (request) => {
97
+ const facts = this.callFacts(this.recorder.nextCallId(), request, null);
98
+ this.emit(this.requestEvent(facts.id, request, null, facts.startedAt));
99
+ this.recorder.settle(facts, { answer: null, outcome: "passthrough", guardsRun: [] });
100
+ },
101
+ mirrorCookiesIntoDocument: (setCookies) => this.browserCookies.mirrorSetCookies(setCookies),
102
+ };
76
103
  /**
77
104
  * The API core, every protocol step of it. Private: the mock's surface is
78
105
  * the app, and a consumer reaching past it would be configuring the
@@ -93,6 +120,8 @@ export class LambderMockApp {
93
120
  registry = new LambderMockEntryRegistry();
94
121
  /** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
95
122
  apiOptions;
123
+ /** The server's schemas, when create() was given the generated table: every entry's input is validated, and every answer parsed, through them. */
124
+ apiSchemas;
96
125
  /** The mock's own guards as create() was given them: which of them need a session, for an entry's mode without the apiOptions table. */
97
126
  mockGuards;
98
127
  /** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
@@ -108,8 +137,24 @@ export class LambderMockApp {
108
137
  declared = null) {
109
138
  this.apiVersion = options.apiVersion ?? null;
110
139
  this.apiOptions = options.apiOptions ?? null;
140
+ this.apiSchemas = options.apiSchemas ? new LambderMockApiSchemas(options.apiSchemas) : null;
111
141
  this.mockGuards = options.guards ?? null;
112
142
  this.guardDeclarations = options.guardDeclarations ?? null;
143
+ // A mock guard stands in for the server's guard of its name, so it
144
+ // declares the codes that guard declares: its own refusals type its
145
+ // ctx.refuse and are what checkApiRefusals holds it to. The compiler
146
+ // compares literal lists at the guards option; this catches a list
147
+ // typed as any string and a caller the compiler did not see.
148
+ for (const [name, guard] of Object.entries(options.guards ?? {})) {
149
+ if (!this.guardDeclarations || !Object.prototype.hasOwnProperty.call(this.guardDeclarations, name))
150
+ continue;
151
+ const own = new Set(Array.isArray(guard?.refusals) ? guard.refusals : []);
152
+ const declared = new Set(this.guardDeclarations[name].refusals ?? []);
153
+ if (own.size !== declared.size || [...declared].some((code) => !own.has(code))) {
154
+ const listed = (codes) => codes.size ? [...codes].map((code) => `"${code}"`).join(", ") : "none";
155
+ throw new Error(`LambderMockApp: the mock guard "${name}" declares the refusals ${listed(own)}, and the server's guard of that name declares ${listed(declared)} in the guardDeclarations table. A mock guard declares exactly the server guard's codes: regenerate the table, or give the mock guard refusals: [${[...declared].map((code) => `"${code}"`).join(", ")}].`);
156
+ }
157
+ }
113
158
  this.refusalVocabulary = declared?.vocabulary ?? null;
114
159
  this.requireRefusalCodes = declared?.requireCodes ?? false;
115
160
  if (this.requireRefusalCodes && !this.apiOptions) {
@@ -117,6 +162,7 @@ export class LambderMockApp {
117
162
  }
118
163
  this.failures = new LambderMockFailureInjector({ apiVersion: this.apiVersion, latency: options.latency ?? 0 });
119
164
  this.recorder = new LambderMockCallRecorder({ callLogSize: options.callLogSize ?? DEFAULT_CALL_LOG_SIZE });
165
+ this.browserCookies.adoptJar(this.pageCookieJar);
120
166
  // The loopback address when nothing names a client, as for a request
121
167
  // from the page itself (the in-process handler transport's default
122
168
  // too). Normalized once here because the direct transport, the MSW
@@ -160,7 +206,7 @@ export class LambderMockApp {
160
206
  ? {
161
207
  manager: new LambderSessionManager({
162
208
  store: sessionOptions.store ?? memorySessionStore,
163
- sessionSalt: sessionOptions.sessionSalt ?? "lambder-mock",
209
+ sessionSalt: sessionOptions.sessionSalt ?? DEFAULT_MOCK_SESSION_SALT,
164
210
  enableSlidingExpiration: sessionOptions.enableSlidingExpiration,
165
211
  slidingWriteIntervalSeconds: sessionOptions.slidingWriteIntervalSeconds,
166
212
  // As given: the manager refuses dataRefresh without
@@ -202,6 +248,9 @@ export class LambderMockApp {
202
248
  callerIdentity: idempotencyOptions.callerIdentity,
203
249
  }
204
250
  : undefined,
251
+ // As on the server: with sessions on, their salt keys the digest
252
+ // of the caller's fields in rate-limit and idempotency keys.
253
+ atRestSecret: sessionOptions ? sessionOptions.sessionSalt ?? DEFAULT_MOCK_SESSION_SALT : undefined,
205
254
  });
206
255
  }
207
256
  // -----------------------------------------------------------------------
@@ -291,6 +340,18 @@ export class LambderMockApp {
291
340
  return null;
292
341
  return this.apiOptions[name].mode;
293
342
  }
343
+ /**
344
+ * The schema an entry's input is validated with: the server's, from the
345
+ * apiSchemas table when create() was given it, then the entry's own where
346
+ * it restates one, run on what the server's leaves (the payload stripped
347
+ * and defaulted as on the server). Either alone where only one is given.
348
+ */
349
+ entryInputOf(name, own) {
350
+ if (!this.apiSchemas)
351
+ return own;
352
+ const server = this.apiSchemas.inputOf(name);
353
+ return own ? server.pipe(own) : server;
354
+ }
294
355
  buildEntry(name, input) {
295
356
  const options = (typeof input === "function" ? { handler: input } : input);
296
357
  const declared = this.declaredOptionsOf(name);
@@ -316,7 +377,7 @@ export class LambderMockApp {
316
377
  // validator that is not a zod schema reach the 422 body as
317
378
  // `zodError: { name: undefined, message: undefined, issues:
318
379
  // undefined }`, a refusal a client cannot read.
319
- input: options.input,
380
+ input: this.entryInputOf(name, options.input),
320
381
  };
321
382
  this.assertEntryRegistration(definition);
322
383
  const handler = options.handler;
@@ -441,18 +502,6 @@ export class LambderMockApp {
441
502
  get registeredNames() {
442
503
  return this.registry.names;
443
504
  }
444
- /**
445
- * Whether a call to this name would be answered from the registry, which
446
- * is what an adapter asks before passing one on.
447
- *
448
- * True for every name once a rest entry is registered, since it answers
449
- * whatever nothing else claimed. That makes a rest entry and the MSW
450
- * adapter's `onUnmocked: "passthrough"` alternatives rather than layers:
451
- * with one registered, nothing is handed on to the network.
452
- */
453
- hasRegisteredEntry(apiName) {
454
- return this.entryFor(apiName) !== null || this.registry.restNotMockedReason !== null;
455
- }
456
505
  entryFor(apiName) {
457
506
  return this.registry.entryFor(apiName);
458
507
  }
@@ -475,7 +524,7 @@ export class LambderMockApp {
475
524
  // -----------------------------------------------------------------------
476
525
  // Control surface
477
526
  // -----------------------------------------------------------------------
478
- /** The next call to the endpoint fails this way; several calls queue in order. An injected refusal names one of the endpoint's declared codes, its data as a handler raises it (see LambderMockRefusalsOf), and is checked and sent as a real one is. */
527
+ /** The next call to the endpoint fails this way; several calls queue in order. An injected refusal names one of the endpoint's declared codes, its data as a handler raises it (see LambderMockRefusalsOf), and is checked and sent as a real one is. An injected sessionExpired, or a refusal whose code is declared so, ends the session the call carries, as the server's does. */
479
528
  failNext(apiName, failure) {
480
529
  this.failures.failNext(apiName, failure);
481
530
  }
@@ -493,13 +542,14 @@ export class LambderMockApp {
493
542
  /**
494
543
  * Rewinds the runtime: sessions, rate-limit counters, replay records,
495
544
  * overrides, injected failures, the offline switch, the configured
496
- * latency, the call log and its numbering, the cookies its own transports
497
- * hold, then onReset, so the app rewinds its own data too.
545
+ * latency, the call log and its numbering, the cookies its own jars hold,
546
+ * then onReset, so the app rewinds its own data too.
498
547
  *
499
548
  * The cookies matter as much as the sessions: a jar still holding the
500
549
  * token of an emptied store's session reads as signed in until an answer
501
- * says sessionExpired. So every jar transport() built for itself is
502
- * emptied, and the cookies a "document" transport mirrored are expired.
550
+ * says sessionExpired. So the page's jar and every jar transport() built
551
+ * for itself are emptied, and the cookies mirrored into document.cookie
552
+ * are expired at the scope each was set at.
503
553
  *
504
554
  * The registry survives, being configuration rather than accumulated
505
555
  * state. Subscriptions survive too, being how a test watches the runtime;
@@ -539,9 +589,10 @@ export class LambderMockApp {
539
589
  /**
540
590
  * Starts a session without a login endpoint: creates it through the
541
591
  * session controller, the way a login handler does, plants its cookies
542
- * into the jar when one is given, so the jar's transport is signed in
543
- * from its next call, and mirrors the readable ones into document.cookie
544
- * the way an answer's cookies are. Returns the raw tokens too.
592
+ * into the jar given, or the page's (pageCookieJar) when none is, so that
593
+ * jar's transport or the MSW adapter is signed in from its next call, and
594
+ * mirrors the readable ones into document.cookie the way an answer's
595
+ * cookies are. Returns the raw tokens too.
545
596
  */
546
597
  async signIn(sessionKey, data, options = {}) {
547
598
  this.assertSessionsConfigured("signIn()");
@@ -560,7 +611,7 @@ export class LambderMockApp {
560
611
  // every Domain against the sending host and refuses one it cannot
561
612
  // check, so unscoped, the session cookie of an app that configures a
562
613
  // cookie domain would be dropped silently.
563
- options.jar?.storeSetCookies(setCookies, { host });
614
+ (options.jar ?? this.pageCookieJar).storeSetCookies(setCookies, { host });
564
615
  // Through the same mirror every other cookie writer uses. Behind the
565
616
  // MSW adapter the page's caller reads its CSRF token from
566
617
  // document.cookie, not the jar, so a signIn that only filled a jar
@@ -571,8 +622,8 @@ export class LambderMockApp {
571
622
  }
572
623
  /**
573
624
  * Ends every session of the subject ("log this subject out everywhere")
574
- * and clears what signIn planted: the cookies in the jar given, and the
575
- * copies in document.cookie.
625
+ * and clears what signIn planted: the cookies in the jar given, or the
626
+ * page's when none is, and the copies in document.cookie.
576
627
  *
577
628
  * Symmetric on purpose, the way reset() is. The records alone leave the
578
629
  * jar and the page carrying a token for a session that no longer exists,
@@ -589,7 +640,7 @@ export class LambderMockApp {
589
640
  serializeClearCookie(this.tokenCookieKey, { ...this.sessionCookieOptions, httpOnly: true }, host),
590
641
  serializeClearCookie(this.csrfCookieKey, this.sessionCookieOptions, host),
591
642
  ];
592
- options.jar?.storeSetCookies(cleared, { host });
643
+ (options.jar ?? this.pageCookieJar).storeSetCookies(cleared, { host });
593
644
  this.browserCookies.mirrorSetCookies(cleared);
594
645
  }
595
646
  /** Marks the subject's session data stale, so the next read renews it through dataRefresh. */
@@ -672,18 +723,6 @@ export class LambderMockApp {
672
723
  at,
673
724
  };
674
725
  }
675
- /**
676
- * Records a call an adapter handed on instead of answering: the MSW
677
- * adapter's passthrough. Without it a name the registry does not know
678
- * leaves no trace at all, and a mistyped endpoint reaches the real
679
- * network with nothing in the call log or on the subscription to say so,
680
- * which is the one failure the log exists to make visible.
681
- */
682
- notePassthrough(request) {
683
- const facts = this.callFacts(this.recorder.nextCallId(), request, null);
684
- this.emit(this.requestEvent(facts.id, request, null, facts.startedAt));
685
- this.recorder.settle(facts, { answer: null, outcome: "passthrough", guardsRun: [] });
686
- }
687
726
  /** What every record of one call repeats (see LambderMockCallFacts). */
688
727
  callFacts(id, request, mode) {
689
728
  return { id, apiName: request.apiName, mode, startedAt: Date.now(), request };
@@ -732,7 +771,21 @@ export class LambderMockApp {
732
771
  // normally.
733
772
  const failure = this.failures.take(request.apiName);
734
773
  if (failure) {
735
- answer = await this.failures.answerFor(failure, request, entry?.definition.refusals);
774
+ const injected = await this.failures.answerFor(failure, request, entry?.definition.refusals);
775
+ // An answer saying the session is over ends the session the
776
+ // call carries, as on the server: there sessionExpired means
777
+ // the read found none, and a refusal flagged so deletes it.
778
+ // Left alive, the caller's next call would be signed in.
779
+ // The cookies stay, as there.
780
+ if (injected.endsSession && this.pipeline.hasSessions) {
781
+ const manager = this.pipeline.sessionManager;
782
+ for (const token of request.cookies[this.tokenCookieKey] ?? []) {
783
+ const session = await manager.lookupSession(token);
784
+ if (session)
785
+ await manager.deleteSession(session);
786
+ }
787
+ }
788
+ answer = injected.answer;
736
789
  outcome = "injected";
737
790
  }
738
791
  else if (prepared) {
@@ -748,7 +801,11 @@ export class LambderMockApp {
748
801
  ? async (callCtx) => {
749
802
  // The payload the handler sees is the restored one.
750
803
  callCtx.payload = request.payload;
751
- const payload = await handler(callCtx);
804
+ const returned = await handler(callCtx);
805
+ // Through the server's output schema when create() was
806
+ // given the table, as the server sends every answer:
807
+ // what it does not declare dropped, its defaults filled.
808
+ const payload = this.apiSchemas ? this.apiSchemas.parseOutput(request.apiName, returned) : returned;
752
809
  // The server's rule, where the server applies it: an
753
810
  // answer is an object or an array, and a handler that
754
811
  // returns anything else has broken its contract.
@@ -870,29 +927,10 @@ export class LambderMockApp {
870
927
  // would keep it out of document.cookie.
871
928
  return transportCarrying(async (request) => {
872
929
  const answer = await withJar(request);
873
- this.mirrorCookiesIntoDocument(answer.setCookies ?? []);
930
+ this.browserCookies.mirrorSetCookies(answer.setCookies ?? []);
874
931
  return answer;
875
932
  }, jar);
876
933
  }
877
- /**
878
- * Mirrors an answer's non-HttpOnly cookies into document.cookie and
879
- * remembers them, so reset() expires them again. The direct transport's
880
- * "document" mode and the MSW adapter both come through here: one
881
- * implementation of the mirror, one record of what was planted.
882
- */
883
- mirrorCookiesIntoDocument(setCookies) {
884
- this.browserCookies.mirrorSetCookies(setCookies);
885
- }
886
- /**
887
- * Takes a jar an adapter built for itself as the runtime's own, so reset()
888
- * empties it with the rest. The MSW adapter's jar holds the session
889
- * cookies of calls that never touch transport(); left full after a reset,
890
- * the next request would carry a token for a session the emptied store no
891
- * longer has.
892
- */
893
- adoptCookieJar(jar) {
894
- this.browserCookies.adoptJar(jar);
895
- }
896
934
  /** caller.setTransport(mockApp.transport(options)); returns the transport, its jar on it. */
897
935
  attach(caller, options = {}) {
898
936
  const transport = this.transport(options);
@@ -918,8 +956,15 @@ export class LambderMockApp {
918
956
  */
919
957
  /** The mock init bound to one vocabulary (or none): the builders, refuse and create() that share it. */
920
958
  const lambderMockInitOf = (declared) => ({
921
- /** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
922
- guard: lambderGuardBuilder(),
959
+ /**
960
+ * Builds a mock guard: the server guard's shape, the handler seeing the
961
+ * mock's contexts. Bound to the vocabulary the mock declared, as the
962
+ * server init's builder is to the server's: its ctx.refuse is typed to the
963
+ * guard's own `refusals`, data in the schema's input form, and a code the
964
+ * vocabulary does not hold is refused as the guard is built. A mock that
965
+ * declared none checks nothing here.
966
+ */
967
+ guard: lambderGuardBuilder(declared ? declared.vocabulary : undefined),
923
968
  /**
924
969
  * Builds a mock rate-limit key, the counterpart of `guard`. Bound to the
925
970
  * mock's own call context, because the server's lambderRateLimitKey() is
@@ -1,4 +1,5 @@
1
1
  import { parseSetCookie } from "../shared/transport/LambderCookieJar.js";
2
+ import { serializeClearCookie } from "../shared/wire/LambderCookie.js";
2
3
  /**
3
4
  * Where the runtime's cookies live outside its own answers: the jars it built
4
5
  * for itself, and the copies it planted in the page's cookie storage. A
@@ -42,13 +43,12 @@ export class LambderMockBrowserCookies {
42
43
  mirrorSetCookies(setCookies) {
43
44
  if (typeof document === "undefined")
44
45
  return;
45
- const secureContext = typeof globalThis.isSecureContext === "boolean" ? globalThis.isSecureContext : true;
46
46
  for (const header of setCookies) {
47
47
  const cookie = parseSetCookie(header, Date.now());
48
48
  if (!cookie || cookie.httpOnly)
49
49
  continue;
50
- document.cookie = secureContext ? header : header.replace(/;\s*Secure\b/i, "");
51
- this.mirroredCookies.set(`${cookie.name}|${cookie.path}`, { name: cookie.name, path: cookie.path });
50
+ writeDocumentCookie(header);
51
+ this.mirroredCookies.set(`${cookie.name}|${cookie.domain ?? ""}|${cookie.path}`, { name: cookie.name, domain: cookie.domain, path: cookie.path });
52
52
  }
53
53
  }
54
54
  /**
@@ -64,9 +64,22 @@ export class LambderMockBrowserCookies {
64
64
  for (const jar of this.ownedJars)
65
65
  jar.clear();
66
66
  if (typeof document !== "undefined") {
67
- for (const cookie of this.mirroredCookies.values())
68
- document.cookie = `${cookie.name}=; Path=${cookie.path}; Max-Age=0`;
67
+ // At the Domain and Path each was set with: a deletion reaches
68
+ // only the cookie of the same name at the same scope, so one
69
+ // planted under the app's cookie domain would outlive a deletion
70
+ // naming the path alone.
71
+ for (const { name, domain, path } of this.mirroredCookies.values())
72
+ writeDocumentCookie(serializeClearCookie(name, { domain, path }));
69
73
  }
70
74
  this.mirroredCookies.clear();
71
75
  }
72
76
  }
77
+ /**
78
+ * One write into document.cookie, `Secure` dropped where the page is not a
79
+ * secure context: the browser refuses such a write there, and plain http on
80
+ * a LAN address has to keep working.
81
+ */
82
+ const writeDocumentCookie = (header) => {
83
+ const secureContext = typeof globalThis.isSecureContext === "boolean" ? globalThis.isSecureContext : true;
84
+ document.cookie = secureContext ? header : header.replace(/;\s*Secure\b/i, "");
85
+ };
@@ -6,6 +6,7 @@ import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
6
6
  import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
7
7
  import type { LambderApiGuard } from "../api/LambderApiGuards.js";
8
8
  import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
9
+ import type { LambderApiSchemaEntry } from "../shared/wire/LambderApiSchemaEntries.js";
9
10
  import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
10
11
  import type { LambderApiRequest } from "../api/LambderApiRequest.js";
11
12
  import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
@@ -141,9 +142,28 @@ export type LambderMockGuardShapeOf<D> = (D extends {
141
142
  } : {
142
143
  session?: false | undefined;
143
144
  });
144
- /** Each mock guard the declarations know held to its declared shape; a guard the table does not have is free. */
145
+ /** The refusal codes a guard or a guard declaration names, as a union: never for one that names none, string for a list typed no narrower. */
146
+ type LambderMockRefusalCodesIn<T> = T extends {
147
+ refusals?: infer R;
148
+ } ? (NonNullable<R> extends readonly (infer N extends string)[] ? N : never) : never;
149
+ /**
150
+ * A mock guard's own `refusals` held to the codes the server's guard of its
151
+ * name declares in the generated table: the same codes, in any order. They
152
+ * type the mock guard's ctx.refuse and are what checkApiRefusals holds the
153
+ * guard to, so a mock guard declaring other codes than the server's would be
154
+ * checked against a guard that does not exist. A list typed as any string,
155
+ * on either side, says nothing to compare; create() compares it.
156
+ */
157
+ type LambderMockGuardRefusalsAgree<TGuard, TDeclaration> = string extends LambderMockRefusalCodesIn<TGuard> | LambderMockRefusalCodesIn<TDeclaration> ? unknown : [LambderMockRefusalCodesIn<TGuard>] extends [LambderMockRefusalCodesIn<TDeclaration>] ? ([LambderMockRefusalCodesIn<TDeclaration>] extends [LambderMockRefusalCodesIn<TGuard>] ? unknown : LambderMockGuardRefusalsMismatch<TDeclaration>) : LambderMockGuardRefusalsMismatch<TDeclaration>;
158
+ /** The error a disagreeing guard meets: the property name is the message, and its value the codes the server's guard declares. */
159
+ type LambderMockGuardRefusalsMismatch<TDeclaration> = {
160
+ readonly "lambder: this mock guard's refusals are not the codes the server's guard of its name declares in guardDeclarations; declare exactly these": TDeclaration extends {
161
+ refusals: infer R;
162
+ } ? R : readonly [];
163
+ };
164
+ /** Each mock guard the declarations know held to its declared shape and refusal codes; a guard the table does not have is free. */
145
165
  type LambderMockGuardsAgree<G, D> = {
146
- [N in keyof G & keyof D]: G[N] extends LambderMockGuardShapeOf<D[N]> ? unknown : LambderMockGuardShapeOf<D[N]>;
166
+ [N in keyof G & keyof D]: (G[N] extends LambderMockGuardShapeOf<D[N]> ? unknown : LambderMockGuardShapeOf<D[N]>) & LambderMockGuardRefusalsAgree<G[N], D[N]>;
147
167
  };
148
168
  /**
149
169
  * The guards option: required whenever the contract declares any guard name,
@@ -211,12 +231,23 @@ export type LambderMockApiOptionsCover<C> = {
211
231
  mode: LambderContractMode<C, K>;
212
232
  };
213
233
  };
234
+ /**
235
+ * What the generated `apiSchemas` table has to hold: an entry for every
236
+ * endpoint the contract declares. A table generated before an endpoint was
237
+ * added is a compile error at the option rather than a throw when that
238
+ * endpoint's entry registers.
239
+ */
240
+ export type LambderMockApiSchemasCover<C> = {
241
+ readonly [K in keyof C & string]: LambderApiSchemaEntry;
242
+ };
214
243
  export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, D extends Record<string, LambderGuardDeclarationEntry> = {}, A extends Record<string, LambderApiOptionEntry> | undefined = undefined> = LambderMockGuardsOption<C, S, G, D> & LambderMockSessionsOption<C, S> & LambderMockIdempotencyOption<C, S, I> & LambderMockRateLimitsOption<C, S, P> & {
215
244
  /**
216
245
  * The server's guard declarations, as the generated options module
217
246
  * exports them (`guardDeclarations`). Given, every mock guard of a name
218
- * the table has is held to its input mode and session requirement at
219
- * the `guards` option (see LambderMockGuardShapeOf). Nothing runs on it.
247
+ * the table has is held to its input mode, session requirement and
248
+ * refusal codes at the `guards` option (see LambderMockGuardShapeOf), and
249
+ * create() compares the refusal codes again, for a list typed no narrower
250
+ * than strings or a caller the compiler did not see. Nothing runs on it.
220
251
  */
221
252
  guardDeclarations?: D;
222
253
  /**
@@ -230,6 +261,22 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
230
261
  * contract (see LambderMockApiOptionsCover).
231
262
  */
232
263
  apiOptions?: A & LambderMockApiOptionsCover<C>;
264
+ /**
265
+ * The server's input and output schemas per API, as the generated schemas
266
+ * module exports them (`apiSchemas`, written by writeApiSchemas). Given,
267
+ * the input of every call a handler answers is validated against the
268
+ * server's input schema and refused as the server refuses it (422, or
269
+ * what onInvalidInput answers), the handler reads the payload as the
270
+ * server's parse leaves it, and every answer is parsed through the
271
+ * server's output schema, which drops what it does not declare and fills
272
+ * its defaults. An entry's own `input` schema still applies, to what the
273
+ * server's leaves. Each schema is rebuilt on the first call that needs
274
+ * it. Refinements and transforms are not in the table (writeApiSchemas
275
+ * lists them). The module names every endpoint: import it from the mock's
276
+ * setup alone, which a production build never loads. The table has to
277
+ * cover the contract (see LambderMockApiSchemasCover).
278
+ */
279
+ apiSchemas?: LambderMockApiSchemasCover<C>;
233
280
  /** Stamped on every answer's envelope as apiVersion, as the server's option is. */
234
281
  apiVersion?: string;
235
282
  /** The version floor, as on the server: a call naming a lower `version` answers versionExpired whatever its signature says. */
@@ -2,10 +2,16 @@ import { type LambderEndpointRefusals } from "../api/LambderApiRefusals.js";
2
2
  import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
3
3
  import type { LambderApiRequest } from "../api/LambderApiRequest.js";
4
4
  import type { LambderMockFailure, LambderMockFailureReason, LambderMockLatency } from "./LambderMockTypes.js";
5
+ /** What an injected failure answers, and whether that answer ends the session the call carries. */
6
+ type LambderMockInjectionResult = {
7
+ answer: LambderApiAnswer;
8
+ endsSession: boolean;
9
+ };
5
10
  /**
6
11
  * A failure the transport reports rather than an answer the caller reads:
7
12
  * an injected network failure or timeout. The caller maps a rejected
8
- * transport to `network`, or `timeout` when its own abort fired.
13
+ * transport to `network`, or to `timeout` or `aborted` when the call's own
14
+ * timeout or signal had aborted it.
9
15
  */
10
16
  export declare class LambderMockTransportError extends Error {
11
17
  readonly reason: "network" | "timeout" | "offline";
@@ -56,9 +62,12 @@ export declare class LambderMockFailureInjector {
56
62
  * never reach the caller as answers. An injected refusal is checked
57
63
  * against the endpoint's declared codes (`endpoint`), as the pipeline
58
64
  * checks a real one, so a test cannot inject what the server could never
59
- * send.
65
+ * send. `endsSession` says the answer tells the caller its session is
66
+ * over (sessionExpired, or a refusal whose code is declared so), which
67
+ * the runtime then makes true.
60
68
  */
61
- answerFor(failure: LambderMockFailure, request: LambderApiRequest, endpoint: LambderEndpointRefusals | undefined): Promise<LambderApiAnswer>;
69
+ answerFor(failure: LambderMockFailure, request: LambderApiRequest, endpoint: LambderEndpointRefusals | undefined): Promise<LambderMockInjectionResult>;
62
70
  /** Clears every injected failure and puts the configured latency back. */
63
71
  reset(): void;
64
72
  }
73
+ export {};
@@ -8,7 +8,8 @@ const toRefusalMessage = (message, fallback) => typeof message === "string" ? {
8
8
  /**
9
9
  * A failure the transport reports rather than an answer the caller reads:
10
10
  * an injected network failure or timeout. The caller maps a rejected
11
- * transport to `network`, or `timeout` when its own abort fired.
11
+ * transport to `network`, or to `timeout` or `aborted` when the call's own
12
+ * timeout or signal had aborted it.
12
13
  */
13
14
  export class LambderMockTransportError extends Error {
14
15
  reason;
@@ -112,16 +113,19 @@ export class LambderMockFailureInjector {
112
113
  * never reach the caller as answers. An injected refusal is checked
113
114
  * against the endpoint's declared codes (`endpoint`), as the pipeline
114
115
  * checks a real one, so a test cannot inject what the server could never
115
- * send.
116
+ * send. `endsSession` says the answer tells the caller its session is
117
+ * over (sessionExpired, or a refusal whose code is declared so), which
118
+ * the runtime then makes true.
116
119
  */
117
120
  async answerFor(failure, request, endpoint) {
118
121
  switch (failure.reason) {
119
122
  case "network": throw new LambderMockTransportError("network");
120
123
  case "timeout": return await this.waitForAbort(request.signal);
121
- case "server": return crashAnswer(this.apiVersion);
124
+ case "server": return { answer: crashAnswer(this.apiVersion), endsSession: false };
122
125
  case "refusal": {
123
126
  const message = toRefusalMessage(failure.message, "Injected refusal.");
124
- return refusalAnswer(checkedRefusal(request.apiName, endpoint, new LambderApiRefusal(message.content, { refusal: message, statusCode: failure.statusCode })), this.apiVersion);
127
+ const checked = checkedRefusal(request.apiName, endpoint, new LambderApiRefusal(message.content, { refusal: message, statusCode: failure.statusCode }));
128
+ return { answer: refusalAnswer(checked, this.apiVersion), endsSession: checked.sessionExpired === true };
125
129
  }
126
130
  case "notAuthorized": {
127
131
  const message = toRefusalMessage(failure.message, "Injected authorization refusal.");
@@ -132,11 +136,11 @@ export class LambderMockFailureInjector {
132
136
  if (!checked.notAuthorized) {
133
137
  throw new Error(`LambderMockApp: the injected notAuthorized failure names the code "${String(message.code)}", which is not declared notAuthorized. Inject it as a refusal, or declare the flag on the code.`);
134
138
  }
135
- return refusalAnswer(checked, this.apiVersion);
139
+ return { answer: refusalAnswer(checked, this.apiVersion), endsSession: checked.sessionExpired === true };
136
140
  }
137
- case "sessionExpired": return sessionExpiredAnswer(this.apiVersion);
138
- case "versionExpired": return versionExpiredAnswer(this.apiVersion);
139
- case "rateLimited": return refusalAnswer(rateLimitRefusal("Injected rate limit.", failure.retryAfterSeconds ?? 30, failure.message), this.apiVersion);
141
+ case "sessionExpired": return { answer: sessionExpiredAnswer(this.apiVersion), endsSession: true };
142
+ case "versionExpired": return { answer: versionExpiredAnswer(this.apiVersion), endsSession: false };
143
+ case "rateLimited": return { answer: refusalAnswer(rateLimitRefusal("Injected rate limit.", { policy: failure.policy ?? "injected", retryAfterSeconds: failure.retryAfterSeconds ?? 30 }, failure.message), this.apiVersion), endsSession: false };
140
144
  }
141
145
  }
142
146
  /** Clears every injected failure and puts the configured latency back. */
@@ -250,9 +250,10 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
250
250
  * 422 exactly as the server would. Optional, and the mock's own: the
251
251
  * contract is type-only, and importing the server's schemas would put the
252
252
  * whole endpoint surface into the browser bundle. Restate it for endpoints
253
- * whose rejection path a test exercises; without it a bad payload reaches
254
- * the handler. What it parses to is pinned to the contract's input (see
255
- * LambderMockInputPin).
253
+ * whose rejection path a test exercises; without it, or the generated
254
+ * `apiSchemas` table given to create() (after which this one runs), a bad
255
+ * payload reaches the handler. What it parses to is pinned to the
256
+ * contract's input (see LambderMockInputPin).
256
257
  */
257
258
  input?: TInputSchema & LambderMockInputPin<C, K, TInputSchema>;
258
259
  handler: LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired>;
@@ -377,7 +378,9 @@ export type LambderMockLatency = number | {
377
378
  * external signal, or for ever, which is what a timeout is). TMessage is the
378
379
  * endpoint's refusal message, so an injected refusal names one of its
379
380
  * declared codes with that code's data, and is checked against the
380
- * declaration as a real one is.
381
+ * declaration as a real one is. An injected rate limit names the policy its
382
+ * data carries (`policy`, default "injected"), so a client's per-policy
383
+ * wording can be tested.
381
384
  */
382
385
  export type LambderMockFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = {
383
386
  reason: "network";
@@ -398,6 +401,7 @@ export type LambderMockFailure<TMessage extends LambderUncheckedRefusalMessage =
398
401
  reason: "versionExpired";
399
402
  } | {
400
403
  reason: "rateLimited";
404
+ policy?: string;
401
405
  retryAfterSeconds?: number;
402
406
  message?: LambderRateLimitMessage;
403
407
  };
@@ -1,6 +1,6 @@
1
1
  import { apiNameOfCallPath } from "../shared/wire/LambderApiNames.js";
2
2
  import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
3
- import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
3
+ import { readApiEnvelopeText, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
4
4
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
5
5
  import { base64ToText } from "../shared/util/LambderBase64.js";
6
6
  import { normalizeClientIp } from "../shared/util/LambderClientIp.js";
@@ -15,13 +15,6 @@ export const lambderMockInvokeTransport = (mockApp, options = {}) => async (even
15
15
  const apiPath = options.apiPath ?? DEFAULT_API_PATH;
16
16
  const rawBody = event.body ?? "";
17
17
  const body = event.isBase64Encoded ? base64ToText(rawBody) : rawBody;
18
- let post = {};
19
- try {
20
- post = JSON.parse(body || "{}") ?? {};
21
- }
22
- catch {
23
- post = {};
24
- }
25
18
  const headers = lowercaseHeaderNames(event.headers);
26
19
  const cookies = cookieValuesByName(event.cookies ?? []);
27
20
  // A JSON POST to `{apiPath}/{group}/{action}` is a call, as the server's
@@ -33,7 +26,7 @@ export const lambderMockInvokeTransport = (mockApp, options = {}) => async (even
33
26
  const apiName = event.requestContext?.http?.method === "POST" && isApiCallContentType(headers)
34
27
  ? apiNameOfCallPath(apiPath, event.rawPath ?? "")
35
28
  : null;
36
- const request = apiName !== null && readApiEnvelope(post, {
29
+ const request = apiName !== null && readApiEnvelopeText(body, {
37
30
  headers, cookies,
38
31
  ip: normalizeClientIp(event.requestContext?.http?.sourceIp ?? ""),
39
32
  host: headers.host || event.requestContext?.domainName || "lambder-invoke",