lambder 11.0.1 → 12.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 (56) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +18 -13
  3. package/dist/api/LambderApiDeclarations.d.ts +194 -0
  4. package/dist/api/LambderApiDeclarations.js +52 -0
  5. package/dist/api/LambderApiDefinition.d.ts +1 -1
  6. package/dist/api/LambderApiGuards.d.ts +21 -22
  7. package/dist/api/LambderApiGuards.js +1 -1
  8. package/dist/api/LambderApiRateLimits.d.ts +19 -19
  9. package/dist/api/LambderApiRateLimits.js +1 -1
  10. package/dist/api/LambderApiRequest.d.ts +15 -6
  11. package/dist/api/LambderApiRequest.js +8 -9
  12. package/dist/build/generatedTables.d.ts +2 -1
  13. package/dist/build/generatedTables.js +1 -1
  14. package/dist/client/LambderCaller.d.ts +36 -23
  15. package/dist/client/LambderCaller.js +88 -67
  16. package/dist/client/lambderFetchTransport.js +2 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.js +2 -0
  19. package/dist/core/Lambder.d.ts +203 -87
  20. package/dist/core/Lambder.js +265 -86
  21. package/dist/core/LambderContext.d.ts +1 -1
  22. package/dist/core/LambderContext.js +13 -5
  23. package/dist/core/LambderCreateOptions.d.ts +28 -39
  24. package/dist/core/LambderCreateOptions.js +2 -3
  25. package/dist/core/LambderPublicFiles.d.ts +8 -1
  26. package/dist/core/LambderPublicFiles.js +1 -1
  27. package/dist/index.d.ts +7 -3
  28. package/dist/index.js +2 -0
  29. package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
  30. package/dist/invoke/LambderInvokeCaller.js +110 -94
  31. package/dist/invoke/LambderLambdaEvent.d.ts +0 -1
  32. package/dist/invoke/LambderLambdaEvent.js +0 -1
  33. package/dist/invoke/lambderHandlerTransport.js +2 -1
  34. package/dist/mock/LambderMockApp.d.ts +31 -25
  35. package/dist/mock/LambderMockApp.js +52 -45
  36. package/dist/mock/LambderMockCreateOptions.d.ts +6 -7
  37. package/dist/mock/LambderMockTypes.d.ts +44 -7
  38. package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
  39. package/dist/mock/lambderMockInvokeTransport.js +15 -7
  40. package/dist/mock/lambderMockMswHandler.js +8 -4
  41. package/dist/mock.d.ts +2 -2
  42. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  43. package/dist/shared/transport/LambderApiTransport.js +2 -3
  44. package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
  45. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  46. package/dist/shared/wire/LambderApiContract.d.ts +4 -18
  47. package/dist/shared/wire/LambderApiContract.js +1 -1
  48. package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
  49. package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
  50. package/dist/shared/wire/LambderApiNames.d.ts +55 -0
  51. package/dist/shared/wire/LambderApiNames.js +81 -0
  52. package/dist/shared/wire/LambderUploadSchemas.js +2 -1
  53. package/dist/testing/LambderTestApp.d.ts +5 -1
  54. package/dist/testing/LambderTestVisitor.d.ts +16 -8
  55. package/dist/testing/LambderTestVisitor.js +33 -25
  56. package/package.json +1 -1
@@ -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;
@@ -220,23 +223,33 @@ export class LambderMockApp {
220
223
  }
221
224
  /**
222
225
  * 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.
226
+ * without a table, and a throw for a name the table does not hold. The
227
+ * compiler already refuses it against the contract; this is where a
228
+ * stale table meets a caller the compiler did not see.
227
229
  */
228
- declaredOptionsOf(name, mode) {
230
+ declaredOptionsOf(name) {
229
231
  if (!this.apiOptions)
230
232
  return null;
231
233
  const declared = Object.prototype.hasOwnProperty.call(this.apiOptions, name) ? this.apiOptions[name] : undefined;
232
234
  if (!declared) {
233
235
  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
236
  }
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
237
  return declared;
239
238
  }
239
+ /**
240
+ * An entry's mode, by the server's rule: a session endpoint when one of
241
+ * its guards needs a session. The guards are the apiOptions table's when
242
+ * create() was given one (which records the mode it came to as well), and
243
+ * otherwise the ones the entry restates, each read off the guardDeclarations
244
+ * table or the mock's own guards.
245
+ */
246
+ modeOf(declared, restatedGuards) {
247
+ if (declared)
248
+ return declared.mode;
249
+ const needsSession = (guard) => (this.guardDeclarations && Object.prototype.hasOwnProperty.call(this.guardDeclarations, guard) && this.guardDeclarations[guard].session === true)
250
+ || (this.mockGuards && Object.prototype.hasOwnProperty.call(this.mockGuards, guard) && this.mockGuards[guard].session === true);
251
+ return toGuardEntries(restatedGuards).some(({ name: guard }) => needsSession(guard)) ? "session" : "public";
252
+ }
240
253
  /**
241
254
  * The codes an entry may refuse with, read off the generated tables (its
242
255
  * own from apiOptions, its guards' from guardDeclarations), each resolved
@@ -276,9 +289,10 @@ export class LambderMockApp {
276
289
  return null;
277
290
  return this.apiOptions[name].mode;
278
291
  }
279
- buildEntry(name, mode, input) {
292
+ buildEntry(name, input) {
280
293
  const options = (typeof input === "function" ? { handler: input } : input);
281
- const declared = this.declaredOptionsOf(name, mode);
294
+ const declared = this.declaredOptionsOf(name);
295
+ const mode = this.modeOf(declared, options.guards);
282
296
  if (declared) {
283
297
  // A caller the compiler did not see (a JavaScript slice, a cast)
284
298
  // would otherwise have its restatement silently lose to the table.
@@ -306,32 +320,34 @@ export class LambderMockApp {
306
320
  const handler = options.handler;
307
321
  return { name, mode, definition, handler: async (ctx) => await handler(ctx), notMockedReason: null };
308
322
  }
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
323
  /**
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.
324
+ * A mock for an endpoint: a handler, or the handler with the endpoint's
325
+ * declarations restated. Its mode is the server's (see modeOf): a
326
+ * session endpoint's session is fetched before the handler runs, and a
327
+ * call without one is refused as on the server.
328
328
  */
329
- notMocked(name, reason) {
330
- return this.buildNotMockedEntry(name, "public", reason);
329
+ api(name, entry) {
330
+ return this.buildEntry(name, entry);
331
331
  }
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);
332
+ /**
333
+ * An endpoint deliberately left without a mock; a call answers the
334
+ * notMocked refusal carrying the reason. The refusal runs through the
335
+ * pipeline, so the steps before dispatch still happen, the session read
336
+ * among them, under the server's mode (see modeOf): a signed-out call to
337
+ * a session endpoint answers sessionExpired as on the server. Without the
338
+ * apiOptions table a session endpoint restates its guards to say so,
339
+ * `notMocked(name, { reason, guards })`; no guard runs.
340
+ */
341
+ notMocked(name, input) {
342
+ const { reason, guards } = (typeof input === "string" ? { reason: input } : input);
343
+ const declared = this.declaredOptionsOf(name);
344
+ if (declared && guards !== undefined) {
345
+ throw new Error(`LambderMockApp: "${name}" restates its guards option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
346
+ }
347
+ const mode = this.modeOf(declared, guards);
348
+ const definition = { name, mode };
349
+ this.assertEntryRegistration(definition);
350
+ return { name, mode, definition, handler: null, notMockedReason: reason };
335
351
  }
336
352
  /**
337
353
  * "Everything I did not register is not mocked, for this reason", as an
@@ -356,18 +372,12 @@ export class LambderMockApp {
356
372
  * a type), and a call it answers is processed as public: the protocol's
357
373
  * pre-pass still runs, so a stale client still hears versionExpired, but
358
374
  * 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.
375
+ * where the server answers sessionExpired. Give create() the table where
376
+ * a test cares about that path.
361
377
  */
362
378
  restNotMocked(reason) {
363
379
  return { restNotMockedReason: reason };
364
380
  }
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
381
  /** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
372
382
  apiSlice(...entries) {
373
383
  const slice = {};
@@ -809,10 +819,7 @@ export class LambderMockApp {
809
819
  // the "saved" record with no call), a Date would stay a Date and a
810
820
  // key set to undefined keep its place, none of which a server sees.
811
821
  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;
822
+ return readApiEnvelope(envelope, info, transportRequest.apiName);
816
823
  }
817
824
  /** One call from a transport request to its answer: what the mock transport and the adapters call. */
818
825
  async handle(transportRequest) {
@@ -16,7 +16,7 @@ import type { LambderSessionStore } from "../shared/contracts/LambderSessionStor
16
16
  import type { LambderSessionDataRefreshConfig } 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>;
@@ -158,9 +158,9 @@ type LambderMockGuardsAgree<G, D> = {
158
158
  type LambderMockGuardsOption<C, S, G, D> = [
159
159
  LambderContractGuardNames<C>
160
160
  ] extends [never] ? {
161
- guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
161
+ guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
162
162
  } : {
163
- guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
163
+ guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
164
164
  };
165
165
  /**
166
166
  * The sessions, idempotency and rateLimits options: each required whenever
@@ -225,10 +225,9 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
225
225
  * exports them (`apiOptions`). Given, every entry's guards, rateLimit and
226
226
  * idempotency are read off the table rather than restated: an entry is
227
227
  * 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
228
+ * a compile error, and the entry takes the table's mode. A notMocked
229
+ * entry and a restNotMocked answer read the endpoint's mode off the table
230
+ * too, so a session endpoint nothing mocks still reads the session first. The table has to cover the
232
231
  * contract (see LambderMockApiOptionsCover).
233
232
  */
234
233
  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
package/dist/mock.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
10
10
  export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
11
11
  export type { LambderMockAppOptions, LambderMockSessionsOptions, LambderMockIdempotencyOptions, LambderMockInvalidInputAnswer, LambderMockTransport, LambderMockTransportOptions } from "./mock/LambderMockCreateOptions.js";
12
- export type { LambderMockCallContext, LambderMockSessionCallContext, LambderMockContext, LambderMockRefusalsOf, LambderMockGuards, LambderMockHandler, LambderMockEntry, LambderMockEntryOptions, LambderMockEntryInput, LambderMockSlice, LambderMockRestEntry, LambderMockRegistryCheck, LambderMockMissingNames, LambderMockStrayNames, LambderMockDuplicateNames, LambderMockPublicNames, LambderMockSessionNames, LambderMockLatency, LambderMockFailure, LambderMockFailureReason, LambderMockOutcome, LambderMockCallEvent, LambderMockRequestEvent, LambderMockResponseEvent, LambderMockCallRecord, LambderMockListener, LambderMockRateLimitPolicies, LambderMockInputOf, LambderMockOutputOf, LambderMockOverride, } from "./mock/LambderMockTypes.js";
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";
13
13
  export { lambderMockPoliciesFrom } from "./mock/lambderMockPoliciesFrom.js";
14
14
  export type { LambderCustomKeyedPolicyNames, LambderMockPolicyKeys } from "./mock/lambderMockPoliciesFrom.js";
15
15
  export type { LambderMockGuardShapeOf } from "./mock/LambderMockCreateOptions.js";
@@ -21,7 +21,7 @@ export { LambderMemoryUploadBucket } from "./stores/LambderMemoryUploadBucket.js
21
21
  export type { LambderMemoryUploadBucketOptions, LambderMemoryUploadObject } from "./stores/LambderMemoryUploadBucket.js";
22
22
  export { lambderMockUploadMswHandler } from "./mock/lambderMockUploadMswHandler.js";
23
23
  export { lambderMockInvokeTransport } from "./mock/lambderMockInvokeTransport.js";
24
- export type { LambderMockInvokeEvent, LambderMockInvokeResult } from "./mock/lambderMockInvokeTransport.js";
24
+ export type { LambderMockInvokeEvent, LambderMockInvokeResult, LambderMockInvokeTransportOptions } from "./mock/lambderMockInvokeTransport.js";
25
25
  export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
26
26
  export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
27
27
  export type { LambderApiTransport, LambderApiTransportRequest } from "./shared/transport/LambderApiTransport.js";
@@ -8,6 +8,7 @@ import type { LambderCompressedGzipPayload, LambderCompressedBrotliPayload } fro
8
8
  */
9
9
  export type LambderApiTransportRequest = {
10
10
  apiPath: string;
11
+ /** The endpoint called, `group.action`: the call goes to `{apiPath}/{group}/{action}` (apiCallPath). */
11
12
  apiName: string;
12
13
  version?: string;
13
14
  /** The caller's signature for this endpoint, out of its LambderApiSignatureMap; absent when it carries no map. */
@@ -90,7 +91,8 @@ export declare const isLambderTransportFailure: (err: unknown) => err is Lambder
90
91
  export type LambderApiTransport = (request: LambderApiTransportRequest) => Promise<LambderApiHttpAnswer>;
91
92
  /**
92
93
  * The fields of the request envelope, in wire order: the one statement of
93
- * what a call sends, for every sender.
94
+ * what a call sends, for every sender. The endpoint is not among them: it is
95
+ * the path the envelope is posted to (apiCallPath).
94
96
  *
95
97
  * Two senders write it. A transport hands the payload over as a value
96
98
  * (buildTransportEnvelope, below); LambderInvokeCaller has already serialized
@@ -101,7 +103,6 @@ export type LambderApiTransport = (request: LambderApiTransportRequest) => Promi
101
103
  * the wire would catch.
102
104
  */
103
105
  export declare const buildEnvelopeFields: (fields: {
104
- apiName: string;
105
106
  version?: string;
106
107
  signature?: string;
107
108
  /** The CSRF token, as the envelope names it. */
@@ -16,7 +16,8 @@ export class LambderTransportFailure extends Error {
16
16
  export const isLambderTransportFailure = (err) => err instanceof Error && err.isLambderTransportFailure === true;
17
17
  /**
18
18
  * The fields of the request envelope, in wire order: the one statement of
19
- * what a call sends, for every sender.
19
+ * what a call sends, for every sender. The endpoint is not among them: it is
20
+ * the path the envelope is posted to (apiCallPath).
20
21
  *
21
22
  * Two senders write it. A transport hands the payload over as a value
22
23
  * (buildTransportEnvelope, below); LambderInvokeCaller has already serialized
@@ -27,7 +28,6 @@ export const isLambderTransportFailure = (err) => err instanceof Error && err.is
27
28
  * the wire would catch.
28
29
  */
29
30
  export const buildEnvelopeFields = (fields) => ({
30
- apiName: fields.apiName,
31
31
  version: fields.version,
32
32
  ...(fields.signature !== undefined ? { signature: fields.signature } : {}),
33
33
  token: fields.token,
@@ -42,7 +42,6 @@ export const buildEnvelopeFields = (fields) => ({
42
42
  * runtime read one shape.
43
43
  */
44
44
  export const buildTransportEnvelope = (request) => buildEnvelopeFields({
45
- apiName: request.apiName,
46
45
  version: request.version,
47
46
  signature: request.signature,
48
47
  token: request.token,
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_SESSION_CSRF_COOKIE_KEY } from "../wire/LambderSessionCookieNames.js";
2
2
  import { resolveApiPathTarget } from "./LambderApiTransport.js";
3
+ import { apiCallPath } from "../wire/LambderApiNames.js";
3
4
  /**
4
5
  * Makes any transport carry a cookie jar the way a browser carries its
5
6
  * cookies: the jar's cookies ride on every request, the answer's Set-Cookie
@@ -36,7 +37,7 @@ export const lambderCookieJarTransport = (inner, options) => {
36
37
  // absolute cross-origin apiPath. siteHost is "" outside a browser, and
37
38
  // an empty host would scope every cookie to nothing while claiming to
38
39
  // scope it.
39
- const target = resolveApiPathTarget(request.apiPath);
40
+ const target = resolveApiPathTarget(apiCallPath(request.apiPath, request.apiName));
40
41
  const cookieScope = {
41
42
  host: target.host ?? options.host ?? (request.siteHost || undefined),
42
43
  path: target.path,
@@ -35,7 +35,7 @@ export type LambderReadonlyDeep<T, TDepth extends unknown[] = []> = TDepth["leng
35
35
  * name, of "this one required and the rest optional".
36
36
  *
37
37
  * An all-optional map is inhabited by `{}`, which would let `guards: {}`
38
- * satisfy requireSessionApiGuards / requirePublicApiGuards at the type level
38
+ * satisfy requireApiGuards at the type level
39
39
  * while declaring no guard: the option is present, so the required-field
40
40
  * check passes, and it normalizes to zero entries, so nothing runs. Requiring
41
41
  * the chosen key also rejects `{ theGuard: undefined }`, which an optional
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Lambder API Contract System
3
3
  *
4
- * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
4
+ * Contracts are the endpoint declarations an instance registered, read with typeof lambder.ApiContract
5
5
  */
6
6
  import type { LambderCrashDetail } from "./LambderCrashDetail.js";
7
7
  import type { LambderUncheckedRefusalMessage, LambderPlainRefusalMessage, LambderRefusalMessage } from "./LambderApiRefusal.js";
8
8
  import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "./LambderApiOptionValues.js";
9
- /** Whether an endpoint runs without a session or requires one (addApi versus addSessionApi). */
9
+ /** Whether an endpoint runs without a session or requires one: "session" exactly when one of its guards needs a session. */
10
10
  export type LambderApiMode = "public" | "session";
11
11
  /**
12
12
  * Base shape for API contracts: what LambderCaller, LambderInvokeCaller and
@@ -15,7 +15,7 @@ export type LambderApiMode = "public" | "session";
15
15
  export type LambderApiContractShape = Record<string, {
16
16
  input: any;
17
17
  output: any;
18
- /** "public" (addApi) or "session" (addSessionApi). */
18
+ /** "public", or "session" when one of its guards needs a session. */
19
19
  mode?: LambderApiMode;
20
20
  /** Present when the API declares guardInput-mode guards: guard name -> value the client must send via options.guardInputs. */
21
21
  guardInputs?: any;
@@ -149,7 +149,7 @@ export type LambderJsonOf<T> = unknown extends T ? T : T extends LambderJsonValu
149
149
  [K in keyof TKeyed as K extends string | number ? (string extends K ? K : number extends K ? K : [LambderJsonOf<TKeyed[K]>] extends [never] ? never : K) : never]: LambderJsonOf<TKeyed[K]>;
150
150
  } : never : never;
151
151
  /**
152
- * One contract entry as addApi/addSessionApi record it: the payload types,
152
+ * One contract entry as defineApi records it: the payload types,
153
153
  * the mode, and every declarative option exactly as written. Options that
154
154
  * were not written are absent rather than undefined, so `keyof` an entry
155
155
  * lists only what the endpoint declared.
@@ -180,20 +180,6 @@ export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInpu
180
180
  }) & ([Idempotency] extends [never] ? {} : {
181
181
  idempotency: Idempotency;
182
182
  });
183
- /**
184
- * Merges a new entry into the contract during chaining.
185
- *
186
- * The contract is therefore an intersection one member deep per endpoint, and
187
- * every `C[K]` read generically (a typed caller, a mock registry, a test
188
- * visitor) resolves the property across all of them. That costs nothing
189
- * worth measuring in a small app and most of a large client's type check,
190
- * which is what writeApiContract (lambder/build) is for: it writes the
191
- * contract out as one object type with plain members, for clients to import
192
- * instead of the server.
193
- */
194
- export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
195
- [K in Name]: Entry;
196
- };
197
183
  /** Guard names referenced by a guards option, whichever of its three forms is used. */
198
184
  export type LambderGuardNamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer N extends string)[] ? N : TOpt extends object ? keyof TOpt & string : never;
199
185
  /** The endpoint's mode; a contract written without one admits either. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Lambder API Contract System
3
3
  *
4
- * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
4
+ * Contracts are the endpoint declarations an instance registered, read with typeof lambder.ApiContract
5
5
  */
6
6
  export {};
@@ -0,0 +1,31 @@
1
+ /** The groups a contract's endpoint names hold: the part of each before its dot. */
2
+ export type LambderContractGroupsOf<TContract> = keyof TContract extends infer TName ? (TName extends `${infer TGroup}.${string}` ? TGroup : never) : never;
3
+ /** The endpoint names of one group. */
4
+ export type LambderContractNamesInGroup<TContract, TGroup extends string> = Extract<keyof TContract, `${TGroup}.${string}`> & string;
5
+ /** An endpoint name's action: the part after its dot. */
6
+ export type LambderContractActionOf<TName> = TName extends `${string}.${infer TAction}` ? TAction : never;
7
+ /** A call made through a group: the endpoint's name, and the arguments as the call site passed them. */
8
+ type LambderGroupCallRunner = (apiName: string, args: unknown[]) => unknown;
9
+ /**
10
+ * The caller, answering for each group a contract could name besides its
11
+ * own members. A member the caller has (a method, a field) is always the
12
+ * caller's; a name that could be a group (see isGroupName: an identifier no
13
+ * caller member has) is one; anything else a runtime or a framework asks of
14
+ * an object (then, toJSON, a symbol, `__v_isRef`) is what the caller itself
15
+ * answers, so a caller is never mistaken for a promise, a ref or a
16
+ * serializable record.
17
+ *
18
+ * A caller keeps its state in #private fields, which no property name
19
+ * reaches, so a group may take any name its public members leave free. Such
20
+ * a field is readable only on the caller itself, never through this proxy,
21
+ * so a member is read and a method run on the caller; a method that returns
22
+ * the caller (setTransport) hands back the proxy, groups and all.
23
+ *
24
+ * A group answers the same way one level down: an ordinary object's own
25
+ * members (toString, valueOf, hasOwnProperty) are an ordinary object's, so
26
+ * converting a group to a string or asking it what it holds calls no
27
+ * endpoint; any other name that could be an action (see isActionName) is
28
+ * one. A group is read-only, like the type that describes it.
29
+ */
30
+ export declare const withApiGroupCalls: <TCaller extends object>(caller: TCaller, call: LambderGroupCallRunner, outcome: LambderGroupCallRunner) => TCaller;
31
+ export {};