lambder 11.1.1 → 13.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 (87) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +25 -15
  3. package/dist/api/LambderApiAnswer.d.ts +7 -0
  4. package/dist/api/LambderApiDeclarations.d.ts +194 -0
  5. package/dist/api/LambderApiDeclarations.js +52 -0
  6. package/dist/api/LambderApiDefinition.d.ts +1 -1
  7. package/dist/api/LambderApiEnvelope.js +4 -1
  8. package/dist/api/LambderApiGuards.d.ts +21 -22
  9. package/dist/api/LambderApiGuards.js +1 -1
  10. package/dist/api/LambderApiRateLimits.d.ts +19 -19
  11. package/dist/api/LambderApiRateLimits.js +1 -1
  12. package/dist/api/LambderApiRefusals.d.ts +22 -0
  13. package/dist/api/LambderApiRefusals.js +13 -0
  14. package/dist/api/LambderApiRequest.d.ts +15 -6
  15. package/dist/api/LambderApiRequest.js +8 -9
  16. package/dist/build/generateApiFiles.d.ts +60 -0
  17. package/dist/build/generateApiFiles.js +85 -0
  18. package/dist/build/generatedTables.d.ts +2 -1
  19. package/dist/build/generatedTables.js +1 -1
  20. package/dist/build.d.ts +4 -1
  21. package/dist/build.js +3 -1
  22. package/dist/client/LambderCaller.d.ts +36 -23
  23. package/dist/client/LambderCaller.js +88 -67
  24. package/dist/client/lambderFetchTransport.js +2 -1
  25. package/dist/client.d.ts +3 -1
  26. package/dist/client.js +2 -0
  27. package/dist/core/Lambder.d.ts +232 -92
  28. package/dist/core/Lambder.js +400 -103
  29. package/dist/core/LambderCallSummary.d.ts +43 -0
  30. package/dist/core/LambderCallSummary.js +10 -0
  31. package/dist/core/LambderContext.d.ts +15 -2
  32. package/dist/core/LambderContext.js +32 -9
  33. package/dist/core/LambderCreateOptions.d.ts +78 -64
  34. package/dist/core/LambderCreateOptions.js +39 -21
  35. package/dist/core/LambderInvocationScope.d.ts +25 -0
  36. package/dist/core/LambderInvocationScope.js +21 -0
  37. package/dist/core/LambderResponse.d.ts +11 -0
  38. package/dist/core/LambderResponse.js +12 -0
  39. package/dist/core/LambderResponseBuilder.js +5 -1
  40. package/dist/index.d.ts +13 -7
  41. package/dist/index.js +3 -1
  42. package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
  43. package/dist/invoke/LambderInvokeCaller.js +110 -94
  44. package/dist/invoke/LambderLambdaEvent.d.ts +2 -1
  45. package/dist/invoke/LambderLambdaEvent.js +8 -3
  46. package/dist/invoke/lambderHandlerTransport.js +2 -1
  47. package/dist/mock/LambderMockApp.d.ts +39 -33
  48. package/dist/mock/LambderMockApp.js +66 -55
  49. package/dist/mock/LambderMockCreateOptions.d.ts +8 -10
  50. package/dist/mock/LambderMockTypes.d.ts +44 -7
  51. package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
  52. package/dist/mock/lambderMockInvokeTransport.js +15 -7
  53. package/dist/mock/lambderMockMswHandler.js +8 -4
  54. package/dist/mock.d.ts +2 -2
  55. package/dist/session/LambderSessionManager.d.ts +46 -3
  56. package/dist/session/LambderSessionManager.js +62 -11
  57. package/dist/shared/LambderI18n.js +49 -4
  58. package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
  59. package/dist/shared/contracts/LambderRateLimiter.js +2 -2
  60. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  61. package/dist/shared/transport/LambderApiTransport.js +2 -3
  62. package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
  63. package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
  64. package/dist/shared/util/LambderInvocationScope.js +5 -0
  65. package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
  66. package/dist/shared/util/LambderNamedMaps.js +19 -0
  67. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  68. package/dist/shared/wire/LambderApiContract.d.ts +4 -18
  69. package/dist/shared/wire/LambderApiContract.js +1 -1
  70. package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
  71. package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
  72. package/dist/shared/wire/LambderApiNames.d.ts +55 -0
  73. package/dist/shared/wire/LambderApiNames.js +81 -0
  74. package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
  75. package/dist/shared/wire/LambderCallOutcome.js +47 -0
  76. package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
  77. package/dist/shared/wire/LambderInvokeApiId.js +10 -0
  78. package/dist/shared/wire/LambderUploadSchemas.js +2 -1
  79. package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
  80. package/dist/stores/LambderDdbRateLimiter.js +64 -45
  81. package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
  82. package/dist/stores/LambderMemoryRateLimiter.js +20 -15
  83. package/dist/testing/LambderTestApp.d.ts +18 -5
  84. package/dist/testing/LambderTestApp.js +21 -5
  85. package/dist/testing/LambderTestVisitor.d.ts +16 -8
  86. package/dist/testing/LambderTestVisitor.js +33 -25
  87. package/package.json +1 -1
@@ -3,7 +3,7 @@ import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.
3
3
  import { type LambderApiRequest } from "../api/LambderApiRequest.js";
4
4
  import { type LambderApiAnswer } from "../api/LambderApiAnswer.js";
5
5
  import { type LambderDeclaredRefuse, type LambderRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
6
- import { type LambderDeclaredVocabulary, type LambderHandlerRefusalsOf, type LambderRefusalVocabulary, type LambderRefusalVocabularyChecks } from "../api/LambderApiRefusals.js";
6
+ import { type LambderDeclaredVocabulary, type LambderHandlerRefusalsOf, type LambderRefusalVocabulary, type LambderRefusalVocabularyOption, type LambderRefusalVocabularyOptionChecks, type LambderMergedRefusalVocabulary } from "../api/LambderApiRefusals.js";
7
7
  import { type LambderApiTransport, type LambderApiTransportRequest } from "../shared/transport/LambderApiTransport.js";
8
8
  import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
9
9
  import { type LambderApiGuard, type LambderGuardBuilder } from "../api/LambderApiGuards.js";
@@ -13,7 +13,7 @@ import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.j
13
13
  import LambderSessionManager, { type LambderCreatedSession } from "../session/LambderSessionManager.js";
14
14
  import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./LambderMockCreateOptions.js";
15
15
  import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
16
- import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockPublicNames, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSessionNames, LambderMockSlice, LambderMockOverride, LambderMockRefusalsOf } from "./LambderMockTypes.js";
16
+ import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockNotMockedInput, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSlice, LambderMockOverride, LambderMockRefusalsOf } from "./LambderMockTypes.js";
17
17
  /**
18
18
  * The mock runtime: the API core (LambderApiPipeline, the same class the
19
19
  * Lambda server runs) over memory stores, with typed mock handlers and mock
@@ -69,6 +69,8 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
69
69
  private readonly registry;
70
70
  /** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
71
71
  private readonly apiOptions;
72
+ /** 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. */
73
+ private readonly mockGuards;
72
74
  /** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
73
75
  private readonly guardDeclarations;
74
76
  /** The server's refusal vocabulary, when create() was given the generated table: whether each code an entry may refuse with carries data. */
@@ -93,12 +95,19 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
93
95
  private assertEntryRegistration;
94
96
  /**
95
97
  * The table's entry for an endpoint, when create() was given one: null
96
- * without a table, and a throw for a name the table does not hold or an
97
- * entry registered under the other mode. The compiler already refuses
98
- * both against the contract; this is where a stale table meets a caller
99
- * the compiler did not see.
98
+ * without a table, and a throw for a name the table does not hold. The
99
+ * compiler already refuses it against the contract; this is where a
100
+ * stale table meets a caller the compiler did not see.
100
101
  */
101
102
  private declaredOptionsOf;
103
+ /**
104
+ * An entry's mode, by the server's rule: a session endpoint when one of
105
+ * its guards needs a session. The guards are the apiOptions table's when
106
+ * create() was given one (which records the mode it came to as well), and
107
+ * otherwise the ones the entry restates, each read off the guardDeclarations
108
+ * table or the mock's own guards.
109
+ */
110
+ private modeOf;
102
111
  /**
103
112
  * The codes an entry may refuse with, read off the generated tables (its
104
113
  * own from apiOptions, its guards' from guardDeclarations), each resolved
@@ -112,25 +121,23 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
112
121
  /** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
113
122
  private declaredModeOf;
114
123
  private buildEntry;
115
- /** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
116
- publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
117
- /** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
118
- sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
119
124
  /**
120
- * A public endpoint deliberately left without a mock; a call answers the
121
- * notMocked refusal carrying the reason.
122
- *
123
- * Public and session get separate builders, as publicApi and sessionApi
124
- * do, because the refusal runs through the pipeline so the steps BEFORE
125
- * dispatch still happen, and the session read is one of them. Were every
126
- * not-mocked endpoint public, a session endpoint with no session would
127
- * answer "not mocked" where the server answers sessionExpired, and its
128
- * events and call log would carry the wrong mode. The contract is a type,
129
- * so the mode cannot be recovered at runtime: the builder has to say it.
125
+ * A mock for an endpoint: a handler, or the handler with the endpoint's
126
+ * declarations restated. Its mode is the server's (see modeOf): a
127
+ * session endpoint's session is fetched before the handler runs, and a
128
+ * call without one is refused as on the server.
129
+ */
130
+ api<K extends keyof C & string, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
131
+ /**
132
+ * An endpoint deliberately left without a mock; a call answers the
133
+ * notMocked refusal carrying the reason. The refusal runs through the
134
+ * pipeline, so the steps before dispatch still happen, the session read
135
+ * among them, under the server's mode (see modeOf): a signed-out call to
136
+ * a session endpoint answers sessionExpired as on the server. Without the
137
+ * apiOptions table a session endpoint restates its guards to say so,
138
+ * `notMocked(name, { reason, guards })`; no guard runs.
130
139
  */
131
- notMocked<K extends LambderMockPublicNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
132
- /** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
133
- sessionNotMocked<K extends LambderMockSessionNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
140
+ notMocked<K extends keyof C & string>(name: K, input: LambderMockNotMockedInput<C, K, TDerived>): LambderMockEntry<C, K>;
134
141
  /**
135
142
  * "Everything I did not register is not mocked, for this reason", as an
136
143
  * argument to the same register() call:
@@ -154,11 +161,10 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
154
161
  * a type), and a call it answers is processed as public: the protocol's
155
162
  * pre-pass still runs, so a stale client still hears versionExpired, but
156
163
  * a signed-out call to an unmocked session endpoint answers "not mocked"
157
- * where the server answers sessionExpired. Declare an endpoint whose
158
- * signed-out path a test cares about with sessionNotMocked there.
164
+ * where the server answers sessionExpired. Give create() the table where
165
+ * a test cares about that path.
159
166
  */
160
167
  restNotMocked(reason: string): LambderMockRestEntry;
161
- private buildNotMockedEntry;
162
168
  /** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
163
169
  apiSlice<const E extends readonly LambderMockEntry<C, keyof C & string>[]>(...entries: E): LambderMockSlice<C, E[number]["name"]>;
164
170
  private addSlices;
@@ -343,16 +349,16 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
343
349
  */
344
350
  export declare const initLambderMock: <C extends LambderApiContractShape, S = any>() => {
345
351
  /**
346
- * Declares the server's refusal vocabulary on the mock: the same object,
347
- * and the same `requireCodes`, the server's init declares, imported from
348
- * shared code (codes, zod schemas, statuses and flags hold nothing
349
- * secret). Given, every refusal an entry answers with is checked and sent
352
+ * Declares the server's refusal vocabulary on the mock: the same map, or
353
+ * list of maps, and the same `requireCodes`, the server's init declares,
354
+ * imported from shared code (codes, zod schemas, statuses and flags hold
355
+ * nothing secret). Given, every refusal an entry answers with is checked and sent
350
356
  * as the server would send it: the code among the codes the tables give
351
357
  * the entry, its data parsed through the code's schema from the input
352
358
  * form, and the declaration's status and flag. An entry whose tables name
353
359
  * a code needs it.
354
360
  */
355
- declareRefusals<const TVocabulary extends LambderRefusalVocabulary, const TRequireCodes extends boolean = false>(refusals: TVocabulary & LambderRefusalVocabularyChecks<TVocabulary>, options?: {
361
+ declareRefusals<const TVocabulary extends LambderRefusalVocabularyOption, const TRequireCodes extends boolean = false>(refusals: TVocabulary & LambderRefusalVocabularyOptionChecks<TVocabulary>, options?: {
356
362
  requireCodes?: TRequireCodes;
357
363
  }): {
358
364
  /** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
@@ -370,7 +376,7 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
370
376
  * codes and `data` the schema's input form, parsed as the server parses
371
377
  * it. The free refuse() where the mock declared none.
372
378
  */
373
- refuse: [TVocabulary] extends [never] ? (content: string, options?: import("../shared/wire/LambderApiRefusal.js").LambderRefuseOptions) => never : LambderDeclaredRefuse<LambderHandlerRefusalsOf<TVocabulary, keyof TVocabulary & string>, TRequireCodes>;
379
+ refuse: [LambderMergedRefusalVocabulary<TVocabulary>] extends [never] ? (content: string, options?: import("../shared/wire/LambderApiRefusal.js").LambderRefuseOptions) => never : LambderDeclaredRefuse<LambderHandlerRefusalsOf<LambderMergedRefusalVocabulary<TVocabulary>, keyof LambderMergedRefusalVocabulary<TVocabulary> & string>, TRequireCodes>;
374
380
  /**
375
381
  * The mock app, with the guard map and the rate-limit policies inferred
376
382
  * from the options.
@@ -382,7 +388,7 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
382
388
  * dropped. `I` exists for the same reason `P` does, and
383
389
  * LambderMockSurplusKeys puts the error back on the key.
384
390
  */
385
- create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, const D extends Record<string, LambderGuardDeclarationEntry> = {}, const A extends Record<string, LambderApiOptionEntry> | undefined = undefined>(options: LambderMockAppOptions<C, S, G, P, I, D, A>): LambderMockApp<C, S, G, [A] extends [undefined] ? false : true, TVocabulary, TRequireCodes>;
391
+ create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, const D extends Record<string, LambderGuardDeclarationEntry> = {}, const A extends Record<string, LambderApiOptionEntry> | undefined = undefined>(options: LambderMockAppOptions<C, S, G, P, I, D, A>): LambderMockApp<C, S, G, [A] extends [undefined] ? false : true, LambderMergedRefusalVocabulary<TVocabulary>, TRequireCodes>;
386
392
  };
387
393
  /** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
388
394
  guard: LambderGuardBuilder<LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
@@ -7,7 +7,7 @@ import { envelopeAnswer, crashAnswer, plainRefusalEnvelope, refusalEnvelope, suc
7
7
  import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
8
8
  import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
9
9
  import { LambderApiRefusal, LAMBDER_REFUSAL_CODES, refuse as refuseApiCall } from "../shared/wire/LambderApiRefusal.js";
10
- import { allowedRefusalOf, readRefusalVocabulary, toRefusalCodes, } from "../api/LambderApiRefusals.js";
10
+ import { allowedRefusalOf, declaredRefusalVocabulary, toRefusalCodes, } from "../api/LambderApiRefusals.js";
11
11
  import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
12
12
  import { buildTransportEnvelope } from "../shared/transport/LambderApiTransport.js";
13
13
  import { lambderCookieJarTransport } from "../shared/transport/lambderCookieJarTransport.js";
@@ -93,6 +93,8 @@ export class LambderMockApp {
93
93
  registry = new LambderMockEntryRegistry();
94
94
  /** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
95
95
  apiOptions;
96
+ /** 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
+ mockGuards;
96
98
  /** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
97
99
  guardDeclarations;
98
100
  /** The server's refusal vocabulary, when create() was given the generated table: whether each code an entry may refuse with carries data. */
@@ -106,6 +108,7 @@ export class LambderMockApp {
106
108
  declared = null) {
107
109
  this.apiVersion = options.apiVersion ?? null;
108
110
  this.apiOptions = options.apiOptions ?? null;
111
+ this.mockGuards = options.guards ?? null;
109
112
  this.guardDeclarations = options.guardDeclarations ?? null;
110
113
  this.refusalVocabulary = declared?.vocabulary ?? null;
111
114
  this.requireRefusalCodes = declared?.requireCodes ?? false;
@@ -160,7 +163,9 @@ export class LambderMockApp {
160
163
  sessionSalt: sessionOptions.sessionSalt ?? "lambder-mock",
161
164
  enableSlidingExpiration: sessionOptions.enableSlidingExpiration,
162
165
  slidingWriteIntervalSeconds: sessionOptions.slidingWriteIntervalSeconds,
163
- dataRefresh: sessionOptions.dataRefresh,
166
+ // As given: the manager refuses dataRefresh without
167
+ // the schema, which an untyped caller can still pass.
168
+ ...{ dataRefresh: sessionOptions.dataRefresh, dataSchema: sessionOptions.dataSchema },
164
169
  // A plain-http page (device testing on a LAN) has no
165
170
  // crypto.subtle, and a memory-only store is nothing
166
171
  // anyone can leak, so hashing there protects nothing.
@@ -220,23 +225,33 @@ export class LambderMockApp {
220
225
  }
221
226
  /**
222
227
  * The table's entry for an endpoint, when create() was given one: null
223
- * without a table, and a throw for a name the table does not hold or an
224
- * entry registered under the other mode. The compiler already refuses
225
- * both against the contract; this is where a stale table meets a caller
226
- * the compiler did not see.
228
+ * without a table, and a throw for a name the table does not hold. The
229
+ * compiler already refuses it against the contract; this is where a
230
+ * stale table meets a caller the compiler did not see.
227
231
  */
228
- declaredOptionsOf(name, mode) {
232
+ declaredOptionsOf(name) {
229
233
  if (!this.apiOptions)
230
234
  return null;
231
235
  const declared = Object.prototype.hasOwnProperty.call(this.apiOptions, name) ? this.apiOptions[name] : undefined;
232
236
  if (!declared) {
233
237
  throw new Error(`LambderMockApp: "${name}" has no entry in the apiOptions table given to create(). The table predates this endpoint: regenerate it with writeApiOptions.`);
234
238
  }
235
- if (declared.mode !== mode) {
236
- throw new Error(`LambderMockApp: "${name}" is a ${declared.mode} endpoint in the apiOptions table, registered here as a ${mode} one.`);
237
- }
238
239
  return declared;
239
240
  }
241
+ /**
242
+ * An entry's mode, by the server's rule: a session endpoint when one of
243
+ * its guards needs a session. The guards are the apiOptions table's when
244
+ * create() was given one (which records the mode it came to as well), and
245
+ * otherwise the ones the entry restates, each read off the guardDeclarations
246
+ * table or the mock's own guards.
247
+ */
248
+ modeOf(declared, restatedGuards) {
249
+ if (declared)
250
+ return declared.mode;
251
+ const needsSession = (guard) => (this.guardDeclarations && Object.prototype.hasOwnProperty.call(this.guardDeclarations, guard) && this.guardDeclarations[guard].session === true)
252
+ || (this.mockGuards && Object.prototype.hasOwnProperty.call(this.mockGuards, guard) && this.mockGuards[guard].session === true);
253
+ return toGuardEntries(restatedGuards).some(({ name: guard }) => needsSession(guard)) ? "session" : "public";
254
+ }
240
255
  /**
241
256
  * The codes an entry may refuse with, read off the generated tables (its
242
257
  * own from apiOptions, its guards' from guardDeclarations), each resolved
@@ -276,9 +291,10 @@ export class LambderMockApp {
276
291
  return null;
277
292
  return this.apiOptions[name].mode;
278
293
  }
279
- buildEntry(name, mode, input) {
294
+ buildEntry(name, input) {
280
295
  const options = (typeof input === "function" ? { handler: input } : input);
281
- const declared = this.declaredOptionsOf(name, mode);
296
+ const declared = this.declaredOptionsOf(name);
297
+ const mode = this.modeOf(declared, options.guards);
282
298
  if (declared) {
283
299
  // A caller the compiler did not see (a JavaScript slice, a cast)
284
300
  // would otherwise have its restatement silently lose to the table.
@@ -306,32 +322,34 @@ export class LambderMockApp {
306
322
  const handler = options.handler;
307
323
  return { name, mode, definition, handler: async (ctx) => await handler(ctx), notMockedReason: null };
308
324
  }
309
- /** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
310
- publicApi(name, entry) {
311
- return this.buildEntry(name, "public", entry);
312
- }
313
- /** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
314
- sessionApi(name, entry) {
315
- return this.buildEntry(name, "session", entry);
316
- }
317
325
  /**
318
- * A public endpoint deliberately left without a mock; a call answers the
319
- * notMocked refusal carrying the reason.
320
- *
321
- * Public and session get separate builders, as publicApi and sessionApi
322
- * do, because the refusal runs through the pipeline so the steps BEFORE
323
- * dispatch still happen, and the session read is one of them. Were every
324
- * not-mocked endpoint public, a session endpoint with no session would
325
- * answer "not mocked" where the server answers sessionExpired, and its
326
- * events and call log would carry the wrong mode. The contract is a type,
327
- * so the mode cannot be recovered at runtime: the builder has to say it.
326
+ * A mock for an endpoint: a handler, or the handler with the endpoint's
327
+ * declarations restated. Its mode is the server's (see modeOf): a
328
+ * session endpoint's session is fetched before the handler runs, and a
329
+ * call without one is refused as on the server.
328
330
  */
329
- notMocked(name, reason) {
330
- return this.buildNotMockedEntry(name, "public", reason);
331
+ api(name, entry) {
332
+ return this.buildEntry(name, entry);
331
333
  }
332
- /** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
333
- sessionNotMocked(name, reason) {
334
- return this.buildNotMockedEntry(name, "session", reason);
334
+ /**
335
+ * An endpoint deliberately left without a mock; a call answers the
336
+ * notMocked refusal carrying the reason. The refusal runs through the
337
+ * pipeline, so the steps before dispatch still happen, the session read
338
+ * among them, under the server's mode (see modeOf): a signed-out call to
339
+ * a session endpoint answers sessionExpired as on the server. Without the
340
+ * apiOptions table a session endpoint restates its guards to say so,
341
+ * `notMocked(name, { reason, guards })`; no guard runs.
342
+ */
343
+ notMocked(name, input) {
344
+ const { reason, guards } = (typeof input === "string" ? { reason: input } : input);
345
+ const declared = this.declaredOptionsOf(name);
346
+ if (declared && guards !== undefined) {
347
+ throw new Error(`LambderMockApp: "${name}" restates its guards option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
348
+ }
349
+ const mode = this.modeOf(declared, guards);
350
+ const definition = { name, mode };
351
+ this.assertEntryRegistration(definition);
352
+ return { name, mode, definition, handler: null, notMockedReason: reason };
335
353
  }
336
354
  /**
337
355
  * "Everything I did not register is not mocked, for this reason", as an
@@ -356,18 +374,12 @@ export class LambderMockApp {
356
374
  * a type), and a call it answers is processed as public: the protocol's
357
375
  * pre-pass still runs, so a stale client still hears versionExpired, but
358
376
  * a signed-out call to an unmocked session endpoint answers "not mocked"
359
- * where the server answers sessionExpired. Declare an endpoint whose
360
- * signed-out path a test cares about with sessionNotMocked there.
377
+ * where the server answers sessionExpired. Give create() the table where
378
+ * a test cares about that path.
361
379
  */
362
380
  restNotMocked(reason) {
363
381
  return { restNotMockedReason: reason };
364
382
  }
365
- buildNotMockedEntry(name, mode, reason) {
366
- this.declaredOptionsOf(name, mode);
367
- const definition = { name, mode };
368
- this.assertEntryRegistration(definition);
369
- return { name, mode, definition, handler: null, notMockedReason: reason };
370
- }
371
383
  /** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
372
384
  apiSlice(...entries) {
373
385
  const slice = {};
@@ -809,10 +821,7 @@ export class LambderMockApp {
809
821
  // the "saved" record with no call), a Date would stay a Date and a
810
822
  // key set to undefined keep its place, none of which a server sees.
811
823
  const envelope = JSON.parse(JSON.stringify(buildTransportEnvelope(transportRequest)));
812
- const request = readApiEnvelope(envelope, info);
813
- if (!request)
814
- throw new Error("LambderMockApp: the transport request names no api.");
815
- return request;
824
+ return readApiEnvelope(envelope, info, transportRequest.apiName);
816
825
  }
817
826
  /** One call from a transport request to its answer: what the mock transport and the adapters call. */
818
827
  async handle(transportRequest) {
@@ -950,19 +959,21 @@ const lambderMockInitOf = (declared) => ({
950
959
  export const initLambderMock = () => ({
951
960
  ...lambderMockInitOf(null),
952
961
  /**
953
- * Declares the server's refusal vocabulary on the mock: the same object,
954
- * and the same `requireCodes`, the server's init declares, imported from
955
- * shared code (codes, zod schemas, statuses and flags hold nothing
956
- * secret). Given, every refusal an entry answers with is checked and sent
962
+ * Declares the server's refusal vocabulary on the mock: the same map, or
963
+ * list of maps, and the same `requireCodes`, the server's init declares,
964
+ * imported from shared code (codes, zod schemas, statuses and flags hold
965
+ * nothing secret). Given, every refusal an entry answers with is checked and sent
957
966
  * as the server would send it: the code among the codes the tables give
958
967
  * the entry, its data parsed through the code's schema from the input
959
968
  * form, and the declaration's status and flag. An entry whose tables name
960
969
  * a code needs it.
961
970
  */
962
971
  declareRefusals(refusals, options = {}) {
963
- const vocabulary = readRefusalVocabulary(refusals);
964
- if (!vocabulary)
965
- throw new Error("LambderMockApp: declareRefusals() takes the vocabulary, an object of codes.");
966
- return lambderMockInitOf({ refusals, vocabulary, requireCodes: (options.requireCodes ?? false) });
972
+ const declared = declaredRefusalVocabulary(refusals, "LambderMockApp");
973
+ return lambderMockInitOf({
974
+ refusals: declared.refusals,
975
+ vocabulary: declared.vocabulary,
976
+ requireCodes: (options.requireCodes ?? false),
977
+ });
967
978
  },
968
979
  });
@@ -13,10 +13,10 @@ import type { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
13
13
  import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
14
14
  import type { LambderRateLimiter, LambderRateLimitWindow } from "../shared/contracts/LambderRateLimiter.js";
15
15
  import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
16
- import type { LambderSessionDataRefreshConfig } from "../session/LambderSessionManager.js";
16
+ import type { LambderSessionDataOptions } from "../session/LambderSessionManager.js";
17
17
  import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
18
18
  import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
19
- import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSurplusKeys } from "./LambderMockTypes.js";
19
+ import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSessionGuardsCheck, LambderMockSurplusKeys } from "./LambderMockTypes.js";
20
20
  export type LambderMockSessionsOptions<S> = {
21
21
  /** Where the mock's sessions rest. Default: a fresh LambderMemorySessionStore. */
22
22
  store?: LambderSessionStore<S>;
@@ -26,13 +26,12 @@ export type LambderMockSessionsOptions<S> = {
26
26
  ttlSeconds?: number;
27
27
  /** Hashing and randomness. Default: WebCrypto where the runtime offers it, the plain stand-in over a memory-only store otherwise. */
28
28
  crypto?: LambderSessionCrypto;
29
- dataRefresh?: LambderSessionDataRefreshConfig<S>;
30
29
  enableSlidingExpiration?: boolean;
31
30
  slidingWriteIntervalSeconds?: number;
32
31
  tokenCookieKey?: string;
33
32
  csrfCookieKey?: string;
34
33
  cookieOptions?: LambderSessionCookieOptions;
35
- };
34
+ } & LambderSessionDataOptions<S>;
36
35
  /**
37
36
  * Idempotency in the mock: the same engine the server runs, over a memory
38
37
  * store unless one is given, and carrying every knob the server's own option
@@ -158,9 +157,9 @@ type LambderMockGuardsAgree<G, D> = {
158
157
  type LambderMockGuardsOption<C, S, G, D> = [
159
158
  LambderContractGuardNames<C>
160
159
  ] extends [never] ? {
161
- guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
160
+ guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
162
161
  } : {
163
- guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
162
+ guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
164
163
  };
165
164
  /**
166
165
  * The sessions, idempotency and rateLimits options: each required whenever
@@ -225,10 +224,9 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
225
224
  * exports them (`apiOptions`). Given, every entry's guards, rateLimit and
226
225
  * idempotency are read off the table rather than restated: an entry is
227
226
  * its handler (and an input schema, if it has one), a restated option is
228
- * a compile error, and an endpoint whose mode the table and the builder
229
- * disagree on is refused at registration. A restNotMocked answer reads
230
- * the endpoint's mode off the table too, so a session endpoint nothing
231
- * mocks still reads the session first. The table has to cover the
227
+ * a compile error, and the entry takes the table's mode. A notMocked
228
+ * entry and a restNotMocked answer read the endpoint's mode off the table
229
+ * too, so a session endpoint nothing mocks still reads the session first. The table has to cover the
232
230
  * contract (see LambderMockApiOptionsCover).
233
231
  */
234
232
  apiOptions?: A & LambderMockApiOptionsCover<C>;
@@ -6,7 +6,7 @@
6
6
  * the contract stays a type-only import in the consuming app.
7
7
  */
8
8
  import type { z } from "zod";
9
- import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
9
+ import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
10
10
  import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
11
11
  import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck } from "../api/LambderApiRateLimits.js";
12
12
  import type { LambderApiCallContext, LambderResponseTools } from "../api/LambderApiCallContext.js";
@@ -105,8 +105,9 @@ export type LambderMockContext<C, K extends keyof C, S, G, TVocabulary = never,
105
105
  * the contract declares, plus, for each guardInput-mode guard, a `guardInput`
106
106
  * schema whose input is what the contract says a client sends. A guard a
107
107
  * public endpoint names may not require a session. A missing name, a schema
108
- * that parses to something else, or such a session guard fails at the
109
- * `guards` option.
108
+ * that parses to something else, or a public endpoint's guard that requires
109
+ * a session fails at the `guards` option; LambderMockSessionGuardsCheck
110
+ * holds the session endpoints to the same rule from the other side.
110
111
  */
111
112
  export type LambderMockGuards<C, S = any> = {
112
113
  [N in LambderContractGuardNames<C>]: LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
@@ -119,6 +120,33 @@ export type LambderMockGuards<C, S = any> = {
119
120
  session?: false;
120
121
  };
121
122
  };
123
+ /** The guards of a mock guard map that need a session. */
124
+ type LambderMockSessionGuardNames<G> = {
125
+ [N in keyof G]: G[N] extends {
126
+ session: true;
127
+ } ? N : never;
128
+ }[keyof G];
129
+ /** The contract's session endpoints none of whose guards the mock guard map declares `session: true`. */
130
+ type LambderMockSessionEndpointsWithoutSessionGuard<C, G> = {
131
+ [K in LambderContractKeysWithMode<C, "session">]: [
132
+ Extract<LambderGuardNamesIn<LambderContractGuardsOf<C, K>>, LambderMockSessionGuardNames<G>>
133
+ ] extends [never] ? K : never;
134
+ }[LambderContractKeysWithMode<C, "session">];
135
+ /**
136
+ * The mock guard map held to the server's rule for session endpoints: each
137
+ * names at least one guard that needs a session. The guards are what make
138
+ * an endpoint a session one, on the server and in the mock alike, and
139
+ * without the apiOptions table the mock reads an entry's mode off the guards
140
+ * it restates, so a session endpoint none of whose mock guards says
141
+ * `session: true` would run as public: a signed-out call would reach a
142
+ * handler typed with its session present. The property name is the message
143
+ * and its value the endpoints at fault.
144
+ */
145
+ export type LambderMockSessionGuardsCheck<C, G> = [
146
+ LambderMockSessionEndpointsWithoutSessionGuard<C, G>
147
+ ] extends [never] ? unknown : {
148
+ readonly "lambder: these session endpoints name no mock guard declared session: true, so the mock would run them as public": LambderMockSessionEndpointsWithoutSessionGuard<C, G>;
149
+ };
122
150
  export type LambderMockHandler<C, K extends keyof C, S, G, TVocabulary = never, TCodesRequired extends boolean = false> = (ctx: LambderMockContext<C, K, S, G, TVocabulary, TCodesRequired>) => LambderMockOutputOf<C, K> | Promise<LambderMockOutputOf<C, K>>;
123
151
  /**
124
152
  * The guards field of an entry: required whenever the contract declares any
@@ -230,7 +258,7 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
230
258
  handler: LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired>;
231
259
  };
232
260
  /**
233
- * What publicApi/sessionApi accept. On a mock created with the generated
261
+ * What api() accepts. On a mock created with the generated
234
262
  * `apiOptions` table (`TDerived`), a bare handler or the options without the
235
263
  * declarations, for every endpoint. Otherwise a bare handler only for an
236
264
  * endpoint the contract declares nothing for, the full options elsewhere, so
@@ -240,6 +268,18 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
240
268
  * bare form.
241
269
  */
242
270
  export type LambderMockEntryInput<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType, TDerived extends boolean = false, TVocabulary = never, TCodesRequired extends boolean = false> = TDerived extends true ? LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired> | LambderMockEntryOptions<C, K, S, G, TInputSchema, true, TVocabulary, TCodesRequired> : [LambderContractGuardsOf<C, K> | LambderContractRateLimitOf<C, K> | LambderContractIdempotencyOf<C, K>] extends [never] ? LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired> | LambderMockEntryOptions<C, K, S, G, TInputSchema, false, TVocabulary, TCodesRequired> : LambderMockEntryOptions<C, K, S, G, TInputSchema, false, TVocabulary, TCodesRequired>;
271
+ /**
272
+ * What notMocked() accepts: the reason, or, for a session endpoint on a mock
273
+ * created without the generated `apiOptions` table, the reason with the
274
+ * endpoint's guards restated. The entry answers before any guard runs, but
275
+ * its mode is still the server's (the session read before the notMocked
276
+ * refusal), and without the table only the guards can say it, as they do
277
+ * for api().
278
+ */
279
+ export type LambderMockNotMockedInput<C, K extends keyof C, TDerived extends boolean = false> = TDerived extends true ? string : LambderContractMode<C, K> extends "session" ? {
280
+ reason: string;
281
+ guards: LambderContractGuardsOf<C, K>;
282
+ } : string;
243
283
  /** One registry entry: the endpoint's definition as the pipeline runs it, and its handler (null when registered as not mocked). */
244
284
  export type LambderMockEntry<C, K extends keyof C & string> = {
245
285
  readonly name: K;
@@ -248,9 +288,6 @@ export type LambderMockEntry<C, K extends keyof C & string> = {
248
288
  readonly handler: ((ctx: any) => Promise<unknown>) | null;
249
289
  readonly notMockedReason: string | null;
250
290
  };
251
- /** The endpoint names a mock app may register under each mode. */
252
- export type LambderMockPublicNames<C> = LambderContractKeysWithMode<C, "public">;
253
- export type LambderMockSessionNames<C> = LambderContractKeysWithMode<C, "session">;
254
291
  /** A slice: the entries of one module, keyed by endpoint name. */
255
292
  export type LambderMockSlice<C, K extends keyof C & string> = {
256
293
  readonly [P in K]: LambderMockEntry<C, P>;
@@ -12,17 +12,25 @@ import { type LambderApiRequest } from "../api/LambderApiRequest.js";
12
12
  * returned function still fits LambderInvokeTransport.
13
13
  */
14
14
  export type LambderMockInvokeEvent = {
15
+ /** The path the call was made to, `{apiPath}/{group}/{action}`. */
16
+ rawPath?: string | undefined;
15
17
  body?: string | undefined;
16
18
  isBase64Encoded?: boolean | undefined;
17
19
  headers?: Record<string, string | undefined> | undefined;
18
20
  cookies?: string[] | undefined;
19
21
  requestContext?: {
20
22
  http?: {
23
+ method?: string;
21
24
  sourceIp?: string;
22
25
  } | undefined;
23
26
  domainName?: string;
24
27
  } | undefined;
25
28
  };
29
+ /** The options of lambderMockInvokeTransport. */
30
+ export type LambderMockInvokeTransportOptions = {
31
+ /** The apiPath the mocked function serves its calls under, as its LambderInvokeCaller names it. Default: "/api", as there. */
32
+ apiPath?: string;
33
+ };
26
34
  /** What the callee answers with: the Lambda response object LambderInvokeCaller decodes. */
27
35
  export type LambderMockInvokeResult = {
28
36
  functionError: string | null;
@@ -43,6 +51,6 @@ export type LambderMockInvokeResult = {
43
51
  */
44
52
  export declare const lambderMockInvokeTransport: (mockApp: {
45
53
  handleRequest(request: LambderApiRequest): Promise<LambderApiAnswer>;
46
- }) => ((event: LambderMockInvokeEvent, options: {
54
+ }, options?: LambderMockInvokeTransportOptions) => ((event: LambderMockInvokeEvent, options: {
47
55
  signal?: AbortSignal;
48
56
  }) => Promise<LambderMockInvokeResult>);
@@ -1,3 +1,5 @@
1
+ import { apiNameOfCallPath } from "../shared/wire/LambderApiNames.js";
2
+ import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
1
3
  import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
2
4
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
3
5
  import { base64ToText } from "../shared/util/LambderBase64.js";
@@ -9,7 +11,8 @@ import { normalizeClientIp } from "../shared/util/LambderClientIp.js";
9
11
  * can then point its typed invoke caller at a mock of the function it
10
12
  * depends on, with the same registry a browser test uses.
11
13
  */
12
- export const lambderMockInvokeTransport = (mockApp) => async (event, { signal }) => {
14
+ export const lambderMockInvokeTransport = (mockApp, options = {}) => async (event, { signal }) => {
15
+ const apiPath = options.apiPath ?? DEFAULT_API_PATH;
13
16
  const rawBody = event.body ?? "";
14
17
  const body = event.isBase64Encoded ? base64ToText(rawBody) : rawBody;
15
18
  let post = {};
@@ -21,16 +24,21 @@ export const lambderMockInvokeTransport = (mockApp) => async (event, { signal })
21
24
  }
22
25
  const headers = lowercaseHeaderNames(event.headers);
23
26
  const cookies = cookieValuesByName(event.cookies ?? []);
24
- // A POST of another type is no API call on the server either. The
25
- // address is the one the synthesized event carries in sourceIp, as the
26
- // server's createContext reads it; no forwarding header is trusted here
27
- // either, so a per-IP limit keys the same address under both adapters.
28
- const request = isApiCallContentType(headers) && readApiEnvelope(post, {
27
+ // A JSON POST to `{apiPath}/{group}/{action}` is a call, as the server's
28
+ // createContext reads it; anything else (another method or type, a path
29
+ // outside apiPath) is no API call on the server either, and a mock
30
+ // serves no routes. The address is the one the synthesized event carries
31
+ // in sourceIp, as createContext reads it; no forwarding header is trusted
32
+ // here either, so a per-IP limit keys the same address under both adapters.
33
+ const apiName = event.requestContext?.http?.method === "POST" && isApiCallContentType(headers)
34
+ ? apiNameOfCallPath(apiPath, event.rawPath ?? "")
35
+ : null;
36
+ const request = apiName !== null && readApiEnvelope(post, {
29
37
  headers, cookies,
30
38
  ip: normalizeClientIp(event.requestContext?.http?.sourceIp ?? ""),
31
39
  host: headers.host || event.requestContext?.domainName || "lambder-invoke",
32
40
  ...(signal ? { signal } : {}),
33
- });
41
+ }, apiName);
34
42
  if (!request) {
35
43
  return { functionError: null, result: { statusCode: 404, headers: { "content-type": "text/plain; charset=utf-8" }, body: "Not found.", isBase64Encoded: false } };
36
44
  }
@@ -1,4 +1,5 @@
1
1
  import { LambderMockTransportError } from "./LambderMockFailureInjector.js";
2
+ import { apiNameOfCallPath } from "../shared/wire/LambderApiNames.js";
2
3
  import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
3
4
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
4
5
  import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
@@ -50,7 +51,9 @@ export const lambderMockMswHandler = (mockApp, options) => {
50
51
  // adapter's is, so one address is one counter under a `per: "ip"` limit
51
52
  // however it was spelled.
52
53
  const clientIp = normalizeClientIp(options.clientIp ?? mockApp.defaultClientIp);
53
- const handler = msw.http.post(apiPath, async ({ request }) => {
54
+ // Every call goes to `{apiPath}/{group}/{action}`; the endpoint is read
55
+ // off the path as the server reads it (apiNameOfCallPath).
56
+ const handler = msw.http.post(`${apiPath.replace(/\/+$/, "")}/:group/:action`, async ({ request }) => {
54
57
  let post;
55
58
  try {
56
59
  post = await request.clone().json();
@@ -91,11 +94,12 @@ export const lambderMockMswHandler = (mockApp, options) => {
91
94
  (cookies[name] ??= []).push(value);
92
95
  }
93
96
  }
97
+ const apiName = apiNameOfCallPath(new URL(apiPath, url).pathname, url.pathname);
98
+ if (apiName === null)
99
+ return undefined;
94
100
  const parsed = readApiEnvelope(post, {
95
101
  headers, cookies, ip: clientIp, host: url.host, signal: request.signal,
96
- });
97
- if (!parsed)
98
- return undefined;
102
+ }, apiName);
99
103
  // Returning undefined hands the request back to MSW, which tries its
100
104
  // other handlers and then the network. Noted on the runtime first:
101
105
  // a passthrough that leaves no event and no call-log row is a