lambder 15.1.1 → 16.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.
@@ -15,6 +15,8 @@ import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
15
15
  import { serializeClearCookie } from "../shared/wire/LambderCookie.js";
16
16
  import { LOOPBACK_CLIENT_IP, normalizeClientIp } from "../shared/util/LambderClientIp.js";
17
17
  import { lambderGuardBuilder, toGuardEntries } from "../api/LambderApiGuards.js";
18
+ import { apiModuleDeclarationsOf } from "../api/LambderApiModules.js";
19
+ import { LambderMockApiModule } from "./LambderMockApiModule.js";
18
20
  import { lambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
19
21
  import { LambderMockFailureInjector, LambderMockTransportError } from "./LambderMockFailureInjector.js";
20
22
  import { LambderMockCallRecorder } from "./LambderMockCallRecorder.js";
@@ -58,6 +60,8 @@ const defaultCookieHost = () => globalThis.location?.host || "localhost";
58
60
  * generated `apiOptions` table, whose entries are their handlers alone.
59
61
  * `TVocabulary` and `TCodesRequired` are the refusal vocabulary the init
60
62
  * declared (declareRefusals), which types every handler's ctx.refuse.
63
+ * `D` is the generated guard declarations create() was given, which a
64
+ * module's mock guards are held to as create()'s are.
61
65
  */
62
66
  export class LambderMockApp {
63
67
  apiVersion;
@@ -122,7 +126,7 @@ export class LambderMockApp {
122
126
  apiOptions;
123
127
  /** The server's schemas, when create() was given the generated table: every entry's input is validated, and every answer parsed, through them. */
124
128
  apiSchemas;
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. */
129
+ /** The mock's own guards as create() was given them, and every registered module's beside them: which of them need a session, for an entry's mode without the apiOptions table. */
126
130
  mockGuards;
127
131
  /** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
128
132
  guardDeclarations;
@@ -138,23 +142,9 @@ export class LambderMockApp {
138
142
  this.apiVersion = options.apiVersion ?? null;
139
143
  this.apiOptions = options.apiOptions ?? null;
140
144
  this.apiSchemas = options.apiSchemas ? new LambderMockApiSchemas(options.apiSchemas) : null;
141
- this.mockGuards = options.guards ?? null;
145
+ this.mockGuards = { ...options.guards };
142
146
  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
- }
147
+ this.assertGuardRefusalsAgree(options.guards ?? {});
158
148
  this.refusalVocabulary = declared?.vocabulary ?? null;
159
149
  this.requireRefusalCodes = declared?.requireCodes ?? false;
160
150
  if (this.requireRefusalCodes && !this.apiOptions) {
@@ -253,6 +243,26 @@ export class LambderMockApp {
253
243
  atRestSecret: sessionOptions ? sessionOptions.sessionSalt ?? DEFAULT_MOCK_SESSION_SALT : undefined,
254
244
  });
255
245
  }
246
+ /**
247
+ * A mock guard stands in for the server's guard of its name, so it
248
+ * declares the codes that guard declares: its own refusals type its
249
+ * ctx.refuse and are what checkApiRefusals holds it to. The compiler
250
+ * compares literal lists at the guards option and at a module's; this
251
+ * catches a list typed as any string and a caller the compiler did not
252
+ * see.
253
+ */
254
+ assertGuardRefusalsAgree(guards) {
255
+ for (const [name, guard] of Object.entries(guards)) {
256
+ if (!this.guardDeclarations || !Object.prototype.hasOwnProperty.call(this.guardDeclarations, name))
257
+ continue;
258
+ const own = new Set(Array.isArray(guard?.refusals) ? guard.refusals : []);
259
+ const declared = new Set(this.guardDeclarations[name].refusals ?? []);
260
+ if (own.size !== declared.size || [...declared].some((code) => !own.has(code))) {
261
+ const listed = (codes) => codes.size ? [...codes].map((code) => `"${code}"`).join(", ") : "none";
262
+ 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(", ")}].`);
263
+ }
264
+ }
265
+ }
256
266
  // -----------------------------------------------------------------------
257
267
  // Registry
258
268
  // -----------------------------------------------------------------------
@@ -266,8 +276,8 @@ export class LambderMockApp {
266
276
  * silently and answer its first call with a 500 naming the SERVER's
267
277
  * option, for a mistake fixed by one option at create().
268
278
  */
269
- assertEntryRegistration(definition) {
270
- this.pipeline.assertRegistration(definition);
279
+ assertEntryRegistration(definition, module) {
280
+ this.pipeline.assertRegistration(definition, module ?? undefined);
271
281
  if (definition.mode === "session" && !this.pipeline.hasSessions) {
272
282
  throw new Error(`LambderMockApp: session endpoint "${definition.name}" needs the sessions option at creation.`);
273
283
  }
@@ -294,11 +304,12 @@ export class LambderMockApp {
294
304
  * otherwise the ones the entry restates, each read off the guardDeclarations
295
305
  * table or the mock's own guards.
296
306
  */
297
- modeOf(declared, restatedGuards) {
307
+ modeOf(declared, restatedGuards, module) {
298
308
  if (declared)
299
309
  return declared.mode;
300
310
  const needsSession = (guard) => (this.guardDeclarations && Object.prototype.hasOwnProperty.call(this.guardDeclarations, guard) && this.guardDeclarations[guard].session === true)
301
- || (this.mockGuards && Object.prototype.hasOwnProperty.call(this.mockGuards, guard) && this.mockGuards[guard].session === true);
311
+ || (Object.prototype.hasOwnProperty.call(this.mockGuards, guard) && this.mockGuards[guard].session === true)
312
+ || (module !== null && Object.prototype.hasOwnProperty.call(module.guards, guard) && module.guards[guard].session === true);
302
313
  return toGuardEntries(restatedGuards).some(({ name: guard }) => needsSession(guard)) ? "session" : "public";
303
314
  }
304
315
  /**
@@ -352,10 +363,15 @@ export class LambderMockApp {
352
363
  const server = this.apiSchemas.inputOf(name);
353
364
  return own ? server.pipe(own) : server;
354
365
  }
355
- buildEntry(name, input) {
366
+ /**
367
+ * An entry built and checked, by the app's api() or by a module's, whose
368
+ * declarations (`module`) its guards may name beside the app's own; the
369
+ * entry carries the module, which registering it adds.
370
+ */
371
+ buildEntry(name, input, module) {
356
372
  const options = (typeof input === "function" ? { handler: input } : input);
357
373
  const declared = this.declaredOptionsOf(name);
358
- const mode = this.modeOf(declared, options.guards);
374
+ const mode = this.modeOf(declared, options.guards, module);
359
375
  if (declared) {
360
376
  // A caller the compiler did not see (a JavaScript slice, a cast)
361
377
  // would otherwise have its restatement silently lose to the table.
@@ -379,9 +395,10 @@ export class LambderMockApp {
379
395
  // undefined }`, a refusal a client cannot read.
380
396
  input: this.entryInputOf(name, options.input),
381
397
  };
382
- this.assertEntryRegistration(definition);
398
+ this.assertEntryRegistration(definition, module);
383
399
  const handler = options.handler;
384
- return { name, mode, definition, handler: async (ctx) => await handler(ctx), notMockedReason: null };
400
+ const entry = { name, mode, definition, handler: async (ctx) => await handler(ctx), notMockedReason: null };
401
+ return module ? { ...entry, module } : entry;
385
402
  }
386
403
  /**
387
404
  * A mock for an endpoint: a handler, or the handler with the endpoint's
@@ -390,7 +407,7 @@ export class LambderMockApp {
390
407
  * call without one is refused as on the server.
391
408
  */
392
409
  api(name, entry) {
393
- return this.buildEntry(name, entry);
410
+ return this.buildEntry(name, entry, null);
394
411
  }
395
412
  /**
396
413
  * An endpoint deliberately left without a mock; a call answers the
@@ -402,14 +419,18 @@ export class LambderMockApp {
402
419
  * `notMocked(name, { reason, guards })`; no guard runs.
403
420
  */
404
421
  notMocked(name, input) {
422
+ return this.buildNotMockedEntry(name, input, null);
423
+ }
424
+ /** A notMocked entry, by the app's notMocked() or by a module's, whose guards give its mode beside the app's own. */
425
+ buildNotMockedEntry(name, input, module) {
405
426
  const { reason, guards } = (typeof input === "string" ? { reason: input } : input);
406
427
  const declared = this.declaredOptionsOf(name);
407
428
  if (declared && guards !== undefined) {
408
429
  throw new Error(`LambderMockApp: "${name}" restates its guards option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
409
430
  }
410
- const mode = this.modeOf(declared, guards);
431
+ const mode = this.modeOf(declared, guards, module);
411
432
  const definition = { name, mode };
412
- this.assertEntryRegistration(definition);
433
+ this.assertEntryRegistration(definition, module);
413
434
  return { name, mode, definition, handler: null, notMockedReason: reason };
414
435
  }
415
436
  /**
@@ -441,6 +462,42 @@ export class LambderMockApp {
441
462
  restNotMocked(reason) {
442
463
  return { restNotMockedReason: reason };
443
464
  }
465
+ /**
466
+ * Defines a module: the mock guards a part of the app declares with its
467
+ * own entries, standing in for the guards the server's module declares
468
+ * (its defineApiModule). Returns the module's entry builders, whose
469
+ * entries may name the app's guards and the module's own; registering
470
+ * them adds the module's guards to the app. So the app's create() need
471
+ * not import every module's mock guards.
472
+ *
473
+ * ```ts
474
+ * export const giftCardMocks = mockApp.defineApiModule({ guards: { cardHolder: mock.guard({ ... }) } });
475
+ * giftCardMocks.apiSlice(giftCardMocks.api("giftCards.redeem", async () => ({ redeemed: true })));
476
+ * ```
477
+ *
478
+ * Each guard is held to what create()'s are held to (the contract, the
479
+ * generated guardDeclarations, surplus keys); a name the app's create()
480
+ * declares is refused here, at compile time and at the call, and a name
481
+ * two modules declare when the second is registered.
482
+ */
483
+ defineApiModule(options) {
484
+ const given = options;
485
+ if (given.rateLimitPolicies !== undefined) {
486
+ throw new Error("LambderMockApp: defineApiModule() takes guards alone. A mock takes every rate-limit policy at create(), the modules' among them, from the generated table (lambderMockPoliciesFrom).");
487
+ }
488
+ const declarations = apiModuleDeclarationsOf({ guards: given.guards }, "LambderMockApp");
489
+ this.assertGuardRefusalsAgree(declarations.guards);
490
+ // Checked against the app now, so a name its create() declares fails
491
+ // where the module is defined; registration checks again, against
492
+ // the other modules.
493
+ this.pipeline.modulesToAdd([{ module: declarations, usedBy: "being defined" }]);
494
+ return new LambderMockApiModule({
495
+ api: (name, entry) => this.buildEntry(name, entry, declarations),
496
+ notMocked: (name, input) => this.buildNotMockedEntry(name, input, declarations),
497
+ apiSlice: (...entries) => this.apiSlice(...entries),
498
+ override: (name, handler) => this.override(name, handler),
499
+ });
500
+ }
444
501
  /** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
445
502
  apiSlice(...entries) {
446
503
  const slice = {};
@@ -451,8 +508,27 @@ export class LambderMockApp {
451
508
  }
452
509
  return slice;
453
510
  }
511
+ /**
512
+ * Registers slices, adding the modules their entries were built with: a
513
+ * module's mock guards are checked against the app's and every other
514
+ * module's first, and taken only once the registry took the slices, so a
515
+ * refused registration leaves the app as it was.
516
+ */
454
517
  addSlices(slices) {
518
+ const uses = [];
519
+ for (const slice of slices) {
520
+ if ("restNotMockedReason" in slice && typeof slice.restNotMockedReason === "string")
521
+ continue;
522
+ for (const entry of Object.values(slice)) {
523
+ const module = entry?.module;
524
+ if (module && !uses.some((use) => use.module === module))
525
+ uses.push({ module, usedBy: `building the entry of "${entry.name}"` });
526
+ }
527
+ }
528
+ const added = this.pipeline.modulesToAdd(uses);
455
529
  this.registry.addSlices(slices);
530
+ this.pipeline.addModules(added);
531
+ this.mockGuards = { ...this.mockGuards, ...added.guards };
456
532
  }
457
533
  /**
458
534
  * Registers the whole contract: every endpoint in exactly one slice, or
@@ -2,8 +2,8 @@ import type { z } from "zod";
2
2
  import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
3
3
  import type { LambderApiRefusalConfig } from "../shared/wire/LambderApiContract.js";
4
4
  import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
5
- import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
6
- import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
5
+ import type { LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
6
+ import type { 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
9
  import type { LambderApiSchemaEntry } from "../shared/wire/LambderApiSchemaEntries.js";
@@ -17,7 +17,7 @@ import type { LambderSessionStore } from "../shared/contracts/LambderSessionStor
17
17
  import type { LambderSessionDataOptions } from "../session/LambderSessionManager.js";
18
18
  import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
19
19
  import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
20
- import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSessionGuardsCheck, LambderMockSurplusKeys } from "./LambderMockTypes.js";
20
+ import type { LambderMockCallContext, LambderMockGuardMapChecks, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSurplusKeys } from "./LambderMockTypes.js";
21
21
  export type LambderMockSessionsOptions<S> = {
22
22
  /** Where the mock's sessions rest. Default: a fresh LambderMemorySessionStore. */
23
23
  store?: LambderSessionStore<S>;
@@ -162,24 +162,24 @@ type LambderMockGuardRefusalsMismatch<TDeclaration> = {
162
162
  } ? R : readonly [];
163
163
  };
164
164
  /** Each mock guard the declarations know held to its declared shape and refusal codes; a guard the table does not have is free. */
165
- type LambderMockGuardsAgree<G, D> = {
165
+ export type LambderMockGuardsAgree<G, D> = {
166
166
  [N in keyof G & keyof D]: (G[N] extends LambderMockGuardShapeOf<D[N]> ? unknown : LambderMockGuardShapeOf<D[N]>) & LambderMockGuardRefusalsAgree<G[N], D[N]>;
167
167
  };
168
168
  /**
169
- * The guards option: required whenever the contract declares any guard name,
170
- * omittable only for a contract that declares none.
169
+ * The guards option: the app's own mock guards, each held to what the
170
+ * contract asks of a guard of its name (LambderMockGuardMapChecks), to the
171
+ * generated declarations when they are given, and to the mock contexts.
171
172
  *
172
- * The same reasoning the entry's own guards field carries: a guard the mock
173
- * does not declare cannot run, so a call the server answers notAuthorized
174
- * would answer 200 here. Optional, it would be the droppable half of exactly
175
- * the check it exists for.
173
+ * It need not name every guard the contract declares, since a part of the
174
+ * app may declare its own with its entries (defineApiModule); what it may
175
+ * not do is leave one out that an entry needs. Every entry is held to having
176
+ * a mock guard for each guard its endpoint declares, in the map it is built
177
+ * with (LambderMockEntryGuardsCheck), because a guard the mock does not
178
+ * declare cannot run, and a call the server answers notAuthorized would
179
+ * answer 200 here.
176
180
  */
177
- type LambderMockGuardsOption<C, S, G, D> = [
178
- LambderContractGuardNames<C>
179
- ] extends [never] ? {
180
- guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
181
- } : {
182
- guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
181
+ type LambderMockGuardsOption<C, S, G, D> = {
182
+ guards?: G & LambderMockGuardMapChecks<C, S, G> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
183
183
  };
184
184
  /**
185
185
  * The sessions, idempotency and rateLimits options: each required whenever
@@ -216,7 +216,7 @@ type LambderMockRateLimitsOption<C, S, P extends LambderMockRateLimitPolicies<S>
216
216
  rateLimits: LambderMockRateLimitsOptions<C, S, P>;
217
217
  };
218
218
  /** Each guard checked for surplus keys, so `sesion: true` on an inline guard is an error at the key rather than a guard that silently runs as public. */
219
- type LambderMockGuardShapes<S, G> = {
219
+ export type LambderMockGuardShapes<S, G> = {
220
220
  [N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
221
221
  };
222
222
  /**
@@ -323,7 +323,7 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
323
323
  * data. Return null for the standard 422 `validation` refusal, which is
324
324
  * also what an app that sets neither gets.
325
325
  */
326
- onInvalidInput?: (zodError: z.ZodError, ctx: LambderMockCallContext<S>) => MaybePromise<LambderMockInvalidInputAnswer | null>;
326
+ onInvalidInput?: (zodError: z.ZodError, ctx: LambderMockCallContext<S>) => LambderMaybePromise<LambderMockInvalidInputAnswer | null>;
327
327
  };
328
328
  /**
329
329
  * What the server's input validation handler answers, as a mock states it:
@@ -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, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
9
+ import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, 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";
@@ -20,6 +20,7 @@ import type { LambderContractRefusalsOf } from "../shared/wire/LambderApiContrac
20
20
  import type { LambderHandlerRefusalsOf } from "../api/LambderApiRefusals.js";
21
21
  import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
22
22
  import type LambderSessionController from "../session/LambderSessionController.js";
23
+ import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
23
24
  export type LambderMockInputOf<C, K extends keyof C> = C[K] extends {
24
25
  input: infer I;
25
26
  } ? I : never;
@@ -101,13 +102,13 @@ export type LambderMockContext<C, K extends keyof C, S, G, TVocabulary = never,
101
102
  refuse: LambderDeclaredRefuse<LambderMockRefusalsOf<C, K, TVocabulary>, TCodesRequired>;
102
103
  };
103
104
  /**
104
- * The guard map a mock app must declare: one guard per name any endpoint of
105
- * the contract declares, plus, for each guardInput-mode guard, a `guardInput`
106
- * schema whose input is what the contract says a client sends. A guard a
107
- * public endpoint names may not require a session. A missing name, a schema
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.
105
+ * What the contract asks of a mock guard of each name it declares: the mock
106
+ * call contexts, plus, for each guardInput-mode guard, a `guardInput` schema
107
+ * whose input is what the contract says a client sends, and, for a guard a
108
+ * public endpoint names, no session. LambderMockGuardMapChecks holds a mock
109
+ * guard map (create()'s, or a module's) to it, name by name; an entry is held
110
+ * to having a mock guard for every guard its endpoint declares
111
+ * (LambderMockEntryGuardsCheck).
111
112
  */
112
113
  export type LambderMockGuards<C, S = any> = {
113
114
  [N in LambderContractGuardNames<C>]: LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
@@ -120,33 +121,50 @@ export type LambderMockGuards<C, S = any> = {
120
121
  session?: false;
121
122
  };
122
123
  };
124
+ /**
125
+ * A mock guard map held, guard by guard, to what the contract asks of the
126
+ * guards it names (LambderMockGuards): a schema that parses to something
127
+ * else, or a public endpoint's guard that requires a session, fails at the
128
+ * map. Guards the contract does not declare are free.
129
+ */
130
+ export type LambderMockGuardMapChecks<C, S, G> = {
131
+ [N in keyof G & keyof LambderMockGuards<C, S>]: LambderMockGuards<C, S>[N];
132
+ };
123
133
  /** The guards of a mock guard map that need a session. */
124
134
  type LambderMockSessionGuardNames<G> = {
125
135
  [N in keyof G]: G[N] extends {
126
136
  session: true;
127
137
  } ? N : never;
128
138
  }[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">];
139
+ /** The guards endpoint K declares that the guard map an entry for it is built with has no mock guard for. */
140
+ type LambderMockGuardsMissing<C, K extends keyof C, G> = Exclude<LambderGuardNamesIn<LambderContractGuardsOf<C, K>>, keyof G>;
141
+ /**
142
+ * Whether an entry for session endpoint K would run as public: without the
143
+ * apiOptions table (`TDerived`) the mock reads an entry's mode off the guards
144
+ * it restates, and none of those the map declares says `session: true`.
145
+ */
146
+ type LambderMockRunsAsPublic<C, K extends keyof C, G, TDerived extends boolean> = TDerived extends true ? false : LambderContractMode<C, K> extends "session" ? ([Extract<LambderGuardNamesIn<LambderContractGuardsOf<C, K>>, LambderMockSessionGuardNames<G>>] extends [never] ? true : false) : false;
147
+ /** The error an entry that would run as public meets on its name. */
148
+ type LambderMockRunsAsPublicError<K> = {
149
+ readonly "lambder: this session endpoint names no mock guard declared session: true, so the mock would run it as public": K;
150
+ };
135
151
  /**
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.
152
+ * An entry held to the guard map it is built with (create()'s, or a
153
+ * module's beside it): each guard its endpoint declares has a mock guard
154
+ * there, since a guard the mock does not declare cannot run and the mock
155
+ * would answer 200 where the server answers notAuthorized; and, where the
156
+ * mock reads the entry's mode off its guards, a session endpoint names one
157
+ * that needs a session, or a signed-out call would reach a handler typed
158
+ * with its session present. Intersected onto the entry's name: the property
159
+ * name is the message, its value the guards or the endpoint at fault.
144
160
  */
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>;
161
+ export type LambderMockEntryGuardsCheck<C, K extends keyof C, G, TDerived extends boolean> = [
162
+ LambderMockGuardsMissing<C, K, G>
163
+ ] extends [never] ? (LambderMockRunsAsPublic<C, K, G, TDerived> extends true ? LambderMockRunsAsPublicError<K> : unknown) : {
164
+ readonly "lambder: these guards of this endpoint have no mock guard where its entry is built; declare them at create(), or build the entry with the defineApiModule() that declares them": LambderMockGuardsMissing<C, K, G>;
149
165
  };
166
+ /** A notMocked entry runs no guard, so it is held only to the mode its restated guards give it (see LambderMockEntryGuardsCheck). */
167
+ export type LambderMockNotMockedGuardsCheck<C, K extends keyof C, G, TDerived extends boolean> = LambderMockRunsAsPublic<C, K, G, TDerived> extends true ? LambderMockRunsAsPublicError<K> : unknown;
150
168
  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>>;
151
169
  /**
152
170
  * The guards field of an entry: required whenever the contract declares any
@@ -288,6 +306,8 @@ export type LambderMockEntry<C, K extends keyof C & string> = {
288
306
  readonly definition: LambderApiDefinition;
289
307
  readonly handler: ((ctx: any) => Promise<unknown>) | null;
290
308
  readonly notMockedReason: string | null;
309
+ /** The module whose builders made the entry (defineApiModule), whose mock guards registering it adds; absent on the app's own. */
310
+ readonly module?: LambderApiModuleDeclarations;
291
311
  };
292
312
  /** A slice: the entries of one module, keyed by endpoint name. */
293
313
  export type LambderMockSlice<C, K extends keyof C & string> = {
package/dist/mock.d.ts CHANGED
@@ -7,9 +7,11 @@
7
7
  * production bundle by construction: nothing else imports it.
8
8
  */
9
9
  export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
10
+ export { LambderMockApiModule } from "./mock/LambderMockApiModule.js";
11
+ export type { LambderMockApiModuleOptions, LambderMockApiModuleChecks, LambderMockApiModuleDoor } from "./mock/LambderMockApiModule.js";
10
12
  export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
11
13
  export type { LambderMockAppOptions, LambderMockSessionsOptions, LambderMockIdempotencyOptions, LambderMockInvalidInputAnswer, LambderMockTransport, LambderMockTransportOptions } from "./mock/LambderMockCreateOptions.js";
12
- export type { LambderMockCallContext, LambderMockSessionCallContext, LambderMockContext, LambderMockRefusalsOf, LambderMockGuards, LambderMockHandler, LambderMockEntry, LambderMockEntryOptions, LambderMockEntryInput, LambderMockNotMockedInput, LambderMockSessionGuardsCheck, LambderMockSlice, LambderMockRestEntry, LambderMockRegistryCheck, LambderMockMissingNames, LambderMockStrayNames, LambderMockDuplicateNames, LambderMockLatency, LambderMockFailure, LambderMockFailureReason, LambderMockOutcome, LambderMockCallEvent, LambderMockRequestEvent, LambderMockResponseEvent, LambderMockCallRecord, LambderMockListener, LambderMockRateLimitPolicies, LambderMockInputOf, LambderMockOutputOf, LambderMockOverride, } from "./mock/LambderMockTypes.js";
14
+ export type { LambderMockCallContext, LambderMockSessionCallContext, LambderMockContext, LambderMockRefusalsOf, LambderMockGuards, LambderMockHandler, LambderMockEntry, LambderMockEntryOptions, LambderMockEntryInput, LambderMockNotMockedInput, LambderMockEntryGuardsCheck, LambderMockNotMockedGuardsCheck, LambderMockSlice, LambderMockRestEntry, LambderMockRegistryCheck, LambderMockMissingNames, LambderMockStrayNames, LambderMockDuplicateNames, LambderMockLatency, LambderMockFailure, LambderMockFailureReason, LambderMockOutcome, LambderMockCallEvent, LambderMockRequestEvent, LambderMockResponseEvent, LambderMockCallRecord, LambderMockListener, LambderMockRateLimitPolicies, LambderMockInputOf, LambderMockOutputOf, LambderMockOverride, } from "./mock/LambderMockTypes.js";
13
15
  export { lambderMockPoliciesFrom } from "./mock/lambderMockPoliciesFrom.js";
14
16
  export type { LambderCustomKeyedPolicyNames, LambderMockPolicyKeys } from "./mock/lambderMockPoliciesFrom.js";
15
17
  export type { LambderMockGuardShapeOf } from "./mock/LambderMockCreateOptions.js";
@@ -34,6 +36,7 @@ export type { LambderSessionCrypto } from "./session/LambderSessionCrypto.js";
34
36
  export { LambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
35
37
  export type { LambderRefusalMessage, LambderDeclaredRefuse, LambderDeclaredRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
36
38
  export { LambderApiRefusalValidationError } from "./api/LambderApiRefusals.js";
39
+ export type { LambderRefusalVocabularyOption, LambderRefusalVocabularyOptionChecks } from "./api/LambderApiRefusals.js";
37
40
  export { assertApiSuccess, assertApiFailure, assertApiRefusal } from "./shared/wire/LambderOutcomeAssertions.js";
38
41
  export type { LambderExpectedFailure } from "./shared/wire/LambderOutcomeAssertions.js";
39
42
  export type { LambderApiRequest } from "./api/LambderApiRequest.js";
package/dist/mock.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * production bundle by construction: nothing else imports it.
8
8
  */
9
9
  export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
10
+ // A part of the mock app with mock guards of its own, declared with its entries.
11
+ export { LambderMockApiModule } from "./mock/LambderMockApiModule.js";
10
12
  // The four collaborators behind LambderMockApp (the entry registry, the call
11
13
  // recorder, the failure injector, the browser cookies) are deliberately not
12
14
  // exported: the runtime is reached through the app. Only the error a
@@ -11,7 +11,7 @@
11
11
  * writes the synchronous form when it has nothing to await, and the framework
12
12
  * awaits either.
13
13
  */
14
- export type MaybePromise<T> = T | Promise<T>;
14
+ export type LambderMaybePromise<T> = T | Promise<T>;
15
15
  /**
16
16
  * T with every object and array readonly, all the way down: what an API
17
17
  * handler's returned answer is checked against.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "15.1.1",
3
+ "version": "16.0.1",
4
4
  "sideEffects": false,
5
5
  "description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
6
6
  "keywords": [