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.
@@ -13,7 +13,7 @@ import { LambderFiles } from "./LambderFiles.js";
13
13
  import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
14
14
  import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
15
15
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
16
- import { declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
16
+ import { assertRefusalCodesDeclared, declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
17
17
  import { refuse } from "../shared/wire/LambderApiRefusal.js";
18
18
  import { apiSignatureOf } from "../api/LambderApiSignature.js";
19
19
  import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
@@ -22,7 +22,10 @@ import { apiNotFoundAnswer, envelopeAnswer, sessionExpiredAnswer, successEnvelop
22
22
  import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
23
23
  import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
24
24
  import { toGuardEntries } from "../api/LambderApiGuards.js";
25
- import { buildApiGroup, buildLazyApiGroup, isRegistrableApiGroup, } from "../api/LambderApiDeclarations.js";
25
+ import { apiModuleDeclarationsOf } from "../api/LambderApiModules.js";
26
+ import { apiModuleUsesOf, isRegistrableApiGroup, } from "../api/LambderApiDeclarations.js";
27
+ import { apiBuildersFor, } from "./LambderApiBuilders.js";
28
+ import { LambderApiModule, } from "./LambderApiModule.js";
26
29
  import { splitApiName } from "../shared/wire/LambderApiNames.js";
27
30
  import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
28
31
  import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
@@ -36,6 +39,8 @@ import { policyBuildersFor } from "./LambderPolicyBuilders.js";
36
39
  import { assertCreateOptions, mergeNamedMaps, } from "./LambderCreateOptions.js";
37
40
  /** The refusals of a name no API is registered under: no code, and none required. */
38
41
  const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
42
+ /** The instance's own declaration builders: no module, and nothing of any instance, so every instance hands out the same three functions, each typed on its own app types. */
43
+ const INSTANCE_BUILDERS = apiBuildersFor(null);
39
44
  /** Longest endpoint name a call summary carries: a page built before paths posts the name in its body, and a line is no place for a long one. */
40
45
  const MAX_SUMMARY_NAME_CHARS = 200;
41
46
  /** A request id as the parent header may carry one: what Lambda's own ids look like, and no longer than any of them. */
@@ -128,9 +133,14 @@ export default class Lambder {
128
133
  apiGroupNames = new Set();
129
134
  /** The lazy groups not loaded yet, each with its load in progress, if one is. */
130
135
  lazyApiGroups = new Map();
131
- /** The guards given at creation, merged into one map, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
136
+ /**
137
+ * The guards given at creation merged into one map, and every registered
138
+ * module's beside them, kept for registration (a guard's session and
139
+ * refusal codes), apiSignatures() (a guard's schema is part of the
140
+ * signature of every endpoint declaring it) and apiOptionEntries().
141
+ */
132
142
  guards;
133
- /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
143
+ /** The rate-limit policies given at creation, and every registered module's beside them, kept for apiOptionEntries(), which records each one less its key handler. */
134
144
  rateLimitPolicies;
135
145
  /** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
136
146
  refusalVocabulary;
@@ -170,7 +180,10 @@ export default class Lambder {
170
180
  const options = {
171
181
  ...given,
172
182
  guards: given.guards && mergeNamedMaps(given.guards, "guard"),
173
- rateLimits: given.rateLimits && { ...given.rateLimits, policies: mergeNamedMaps(given.rateLimits.policies, "rate-limit policy") },
183
+ rateLimits: given.rateLimits && {
184
+ ...given.rateLimits,
185
+ policies: given.rateLimits.policies === undefined ? undefined : mergeNamedMaps(given.rateLimits.policies, "rate-limit policy"),
186
+ },
174
187
  };
175
188
  assertCreateOptions(options);
176
189
  this.files = options.files ? new LambderFiles(options.files) : null;
@@ -189,8 +202,8 @@ export default class Lambder {
189
202
  this.corsConfig = options.cors === true ? {} : options.cors;
190
203
  }
191
204
  const session = options.session;
192
- this.guards = options.guards;
193
- this.rateLimitPolicies = options.rateLimits?.policies;
205
+ this.guards = { ...options.guards };
206
+ this.rateLimitPolicies = { ...options.rateLimits?.policies };
194
207
  this.refusalVocabulary = readRefusalVocabulary(options.refusals);
195
208
  this.pipeline = new LambderApiPipeline({
196
209
  apiVersion: this.apiVersion,
@@ -341,46 +354,56 @@ export default class Lambder {
341
354
  // Declared as values, gathered into named groups, registered in one call.
342
355
  // =====================================================================
343
356
  /**
344
- * Declares one endpoint, typed on this instance's types: its input and
345
- * output schemas, its guards, rate limits, idempotency and refusals, and
346
- * its handler. Nothing is registered until the declaration is put in a
347
- * group (defineApiGroup) and the group is registered (registerApiGroups).
357
+ * Declares one endpoint, typed on this instance's types (see
358
+ * LambderDefineApi). Nothing is registered until the declaration is put
359
+ * in a group (defineApiGroup) and the group is registered
360
+ * (registerApiGroups).
348
361
  *
349
- * The guards say who may call it, and so its mode: an endpoint whose
350
- * guards include one that needs a session (`session: true`) is a session
351
- * endpoint, its session read before the guards run and a call without
352
- * one answered sessionExpired, and its handler's ctx.session is typed
353
- * present. Any other endpoint is public.
354
- *
355
- * An arrow property rather than a method, so an app can hand it out
356
- * detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
362
+ * An arrow property rather than a method, as are the two below, so an
363
+ * app can hand them out detached; handed out under the instance's name
364
+ * (`export const defineApi: AppLambder["defineApi"] = lambderApp.defineApi`),
365
+ * a declaration file names each by the instance's type rather than
366
+ * writing the app's types out again in each.
357
367
  */
358
- defineApi = (options,
359
- /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
360
- handler) => ({ kind: "lambderApi", options: options, handler: handler });
368
+ defineApi = INSTANCE_BUILDERS.defineApi;
369
+ /** Gathers endpoints into a named group (see LambderDefineApiGroup). */
370
+ defineApiGroup = INSTANCE_BUILDERS.defineApiGroup;
371
+ /** A group loaded on the first call to one of its endpoints (see LambderDefineLazyApiGroup). */
372
+ lazyApiGroup = INSTANCE_BUILDERS.lazyApiGroup;
361
373
  /**
362
- * Gathers endpoints into a named group: each is registered as
363
- * `name.action` and called at `{apiPath}/{name}/{action}`, and every
364
- * Lambder caller reaches it as `caller.name.action(input)`. A group's
365
- * name and its actions are identifiers, and a group may not take a name
366
- * a caller already answers for (see LAMBDER_RESERVED_GROUP_NAMES).
374
+ * Defines a module: guards and rate-limit policies a part of the app
375
+ * declares for its own endpoints, beside the ones this instance was
376
+ * created with. Returns the module's builders, whose endpoints may name
377
+ * the instance's guards and policies and the module's own, and nothing
378
+ * else; their groups are registered with registerApiGroups() as any
379
+ * are, and their registration adds the module's guards and policies to
380
+ * the instance that registers them.
367
381
  *
368
- * A group declared across files takes each file's part:
369
- * `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action
370
- * two parts declare is refused, at compile time and at startup, rather
371
- * than one silently replacing the other as an object spread would.
372
- */
373
- defineApiGroup = (name, ...parts) => buildApiGroup(name, parts);
374
- /**
375
- * A group loaded on the first call to one of its endpoints:
376
- * `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
377
- * A cold start then parses none of it, nor anything only it imports,
378
- * until a request calls it; the contract is the loaded group's, read off
379
- * its type. The group loaded must carry the same name, which the type
380
- * checks and loading checks again. `loadApiGroups()` loads every group,
381
- * for a build step or a boot check that has to see them all.
382
+ * ```typescript
383
+ * export const giftCardModule = storeApp.defineApiModule({
384
+ * guards: { cardHolder },
385
+ * rateLimitPolicies: { redeemPerCard: { perHour: 5, per: "session" } },
386
+ * });
387
+ * ```
388
+ *
389
+ * A name the instance declares is refused here, at compile time and at
390
+ * the call; a name two modules declare is refused when the second is
391
+ * registered, as create() refuses a name two maps declare. A module's
392
+ * rate-limit policies count on the instance's limiter, so they need the
393
+ * rateLimits option at creation, whose own policies may then be left out.
382
394
  */
383
- lazyApiGroup = (name, load) => buildLazyApiGroup(name, load);
395
+ defineApiModule(options) {
396
+ const given = options;
397
+ const declarations = apiModuleDeclarationsOf(given, "Lambder");
398
+ for (const [name, guard] of Object.entries(declarations.guards)) {
399
+ assertRefusalCodesDeclared(`guard "${name}"`, (guard?.refusals ?? []), this.refusalVocabulary);
400
+ }
401
+ // Checked against this instance now, so a name it declares fails
402
+ // where the module is defined; registration checks again against
403
+ // whichever instance takes the module, and against the other modules.
404
+ this.pipeline.modulesToAdd([{ module: declarations, usedBy: "being defined" }]);
405
+ return new LambderApiModule(declarations);
406
+ }
384
407
  /**
385
408
  * Registers groups of endpoints, as one contract: `typeof
386
409
  * lambder.ApiContract` then maps every `group.action` to its entry, one
@@ -411,7 +434,6 @@ export default class Lambder {
411
434
  // caller that catches the error and fixes the declaration is told the
412
435
  // real problem on its next try.
413
436
  const names = new Set();
414
- const prepared = [];
415
437
  for (const group of groups) {
416
438
  if (!isRegistrableApiGroup(group)) {
417
439
  throw new Error("Lambder: registerApiGroups takes groups built by defineApiGroup() or lazyApiGroup().");
@@ -420,10 +442,21 @@ export default class Lambder {
420
442
  throw new Error(`Lambder: group "${group.name}" is registered twice. A group is one namespace, so its endpoints belong in one defineApiGroup().`);
421
443
  }
422
444
  names.add(group.name);
445
+ }
446
+ // The modules the groups bring, a lazy group's at once: checked
447
+ // against the instance and each other here, taken only after every
448
+ // endpoint passed against them.
449
+ const registrable = groups;
450
+ const added = instance.pipeline.modulesToAdd(registrable.flatMap(apiModuleUsesOf));
451
+ const prepared = [];
452
+ for (const group of registrable) {
423
453
  if (group.kind === "lambderApiGroup")
424
- prepared.push(...instance.prepareApiGroup(group));
454
+ prepared.push(...instance.prepareApiGroup(group, added));
425
455
  }
426
- for (const group of groups) {
456
+ instance.pipeline.addModules(added);
457
+ instance.guards = { ...instance.guards, ...added.guards };
458
+ instance.rateLimitPolicies = { ...instance.rateLimitPolicies, ...added.rateLimitPolicies };
459
+ for (const group of registrable) {
427
460
  instance.apiGroupNames.add(group.name);
428
461
  if (group.kind === "lambderLazyApiGroup")
429
462
  instance.lazyApiGroups.set(group.name, { group, loading: null });
@@ -478,9 +511,14 @@ export default class Lambder {
478
511
  await this.loadLazyApiGroup(group);
479
512
  return this.apiDefinitions.get(apiName);
480
513
  }
481
- /** The endpoints of one group, each checked as `group.action`; nothing is registered until they are committed. */
482
- prepareApiGroup(group) {
483
- return Object.entries(group.apis).map(([action, declaration]) => this.prepareApi(`${group.name}.${action}`, declaration.options, declaration.handler));
514
+ /**
515
+ * The endpoints of one group, each checked as `group.action` against the
516
+ * instance's guards and policies and those `adding` holds, of the modules
517
+ * the same registration adds; nothing is registered until they are
518
+ * committed.
519
+ */
520
+ prepareApiGroup(group, adding) {
521
+ return Object.entries(group.apis).map(([action, declaration]) => this.prepareApi(`${group.name}.${action}`, declaration.options, declaration.handler, adding));
484
522
  }
485
523
  /**
486
524
  * A lazy group, loaded once: its import, checked to be the group it was
@@ -501,6 +539,16 @@ export default class Lambder {
501
539
  if (loaded.name !== name) {
502
540
  throw new Error(`Lambder: lazy group "${name}" loaded the group "${loaded.name}". The loader must resolve to the group of the name it was registered under.`);
503
541
  }
542
+ // A module's guards and policies are added when its group is
543
+ // registered, at startup, so a collision fails the boot and a
544
+ // route charging one of its policies finds it from the first
545
+ // request; one first met here would do neither.
546
+ for (const { module } of apiModuleUsesOf(loaded)) {
547
+ if (!this.pipeline.hasModule(module)) {
548
+ throw new Error(`Lambder: lazy group "${name}" loaded endpoints a module's builders declared, and was registered with a lazyApiGroup that does not carry that module. ` +
549
+ `Register it with the module's own lazyApiGroup, so its guards and rate-limit policies are added at startup.`);
550
+ }
551
+ }
504
552
  for (const api of this.prepareApiGroup(loaded))
505
553
  this.commitApi(api);
506
554
  this.lazyApiGroups.delete(name);
@@ -517,9 +565,12 @@ export default class Lambder {
517
565
  * digests, beside its handler. Nothing is recorded here (commitApi does
518
566
  * that), so a refusal anywhere in a registration leaves nothing behind.
519
567
  */
520
- prepareApi(name, schema, handler) {
568
+ prepareApi(name, schema, handler, adding) {
521
569
  const guardEntries = toGuardEntries(schema.guards);
522
- const guardNeedsSession = (guard) => !!this.guards && Object.prototype.hasOwnProperty.call(this.guards, guard) && this.guards[guard]?.session === true;
570
+ const guardOf = (guard) => Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]
571
+ : adding && Object.prototype.hasOwnProperty.call(adding.guards, guard) ? adding.guards[guard]
572
+ : undefined;
573
+ const guardNeedsSession = (guard) => guardOf(guard)?.session === true;
523
574
  const mode = guardEntries.some(({ name: guard }) => guardNeedsSession(guard)) ? "session" : "public";
524
575
  if (mode === "session" && !this.pipeline.hasSessions) {
525
576
  throw new Error(`Lambder: a guard of API "${name}" needs a session, and the instance was created without the session option.`);
@@ -534,13 +585,10 @@ export default class Lambder {
534
585
  if (schema.refusals !== undefined && ownRefusals.length === 0) {
535
586
  throw new Error(`Lambder: API "${name}" declares an empty refusals option, which declares no code. Name the codes it refuses with, or omit the option entirely.`);
536
587
  }
537
- const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({
538
- guard,
539
- codes: (this.guards && Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]?.refusals : undefined) ?? [],
540
- })));
588
+ const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({ guard, codes: guardOf(guard)?.refusals ?? [] })));
541
589
  const refusals = { codes: allowedCodes, codeRequired: this.requireRefusalCodes };
542
590
  const definition = { name, mode, guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output, refusals };
543
- this.pipeline.assertRegistration(definition);
591
+ this.pipeline.assertRegistration(definition, adding);
544
592
  return { definition, refusalsOption: schema.refusals, output: schema.output, compress: schema.compress ?? "auto", handler };
545
593
  }
546
594
  /** Records a checked endpoint: its definition, its refusals option as written, and what answers it. */
@@ -703,7 +751,7 @@ export default class Lambder {
703
751
  apis[name] = entry;
704
752
  }
705
753
  const rateLimitPolicies = {};
706
- for (const name of Object.keys(this.rateLimitPolicies ?? {}).sort()) {
754
+ for (const name of Object.keys(this.rateLimitPolicies).sort()) {
707
755
  const { per, budget, chargeAt, refusal, ...windows } = this.rateLimitPolicies[name];
708
756
  const entry = {};
709
757
  for (const [window, limit] of Object.entries(windows)) {
@@ -723,7 +771,7 @@ export default class Lambder {
723
771
  rateLimitPolicies[name] = entry;
724
772
  }
725
773
  const guards = {};
726
- for (const name of Object.keys(this.guards ?? {}).sort()) {
774
+ for (const name of Object.keys(this.guards).sort()) {
727
775
  const guard = this.guards[name];
728
776
  guards[name] = {
729
777
  input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
@@ -0,0 +1,105 @@
1
+ import type { z } from "zod";
2
+ import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
3
+ import type { LambderObjectOutputCheck, LambderPayloadSliceCheck, LambderRequirableGuardsField } from "./LambderCreateOptions.js";
4
+ import type { LambderReadonlyDeep, LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
5
+ import type { LambderDeclaredRefuse } from "../shared/wire/LambderApiRefusal.js";
6
+ import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
7
+ import type { LambderContractEntry, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
8
+ import type { LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf, LambderGuardsOption } from "../api/LambderApiGuards.js";
9
+ import type { LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
10
+ import type { LambderHandlerRefusalsOf, LambderRefusalNamesIn, LambderRefusalsOption, LambderWireRefusalsOf } from "../api/LambderApiRefusals.js";
11
+ import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
12
+ import { type LambderActionNamesCheck, type LambderApiDeclaration, type LambderApiDeclarations, type LambderApiGroup, type LambderApiModeOf, type LambderAppTypes, type LambderGroupNameCheck, type LambderGroupPartsCheck, type LambderLazyApiGroup, type LambderMergedParts, type LambderPublicSessionPolicyCheck, type LambderSessionModeCheck } from "../api/LambderApiDeclarations.js";
13
+ /** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
14
+ export type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
15
+ /**
16
+ * What an endpoint's handler is handed: the session context when one of its
17
+ * guards needs a session (so ctx.session is typed present), the plain one
18
+ * otherwise, with the payload, the guards' data and a refuse() typed to the
19
+ * codes the endpoint may refuse with.
20
+ */
21
+ export type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt, TRefusalsOpt> = (LambderApiModeOf<TApp["guards"], TGuardsOpt> extends "session" ? LambderSessionRenderContext<TPayload, TApp["session"], Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["policies"]> : LambderRenderContext<TPayload, Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["session"], TApp["policies"]>) & {
22
+ refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
23
+ };
24
+ /**
25
+ * Declares one endpoint, typed on one set of app types: its input and output
26
+ * schemas, its guards, rate limits, idempotency and refusals, and its
27
+ * handler. Nothing is registered until the declaration is put in a group
28
+ * (defineApiGroup) and the group is registered (registerApiGroups).
29
+ *
30
+ * The guards say who may call it, and so its mode: an endpoint whose guards
31
+ * include one that needs a session (`session: true`) is a session endpoint,
32
+ * its session read before the guards run and a call without one answered
33
+ * sessionExpired, and its handler's ctx.session is typed present. Any other
34
+ * endpoint is public.
35
+ */
36
+ export type LambderDefineApi<TApp extends LambderAppTypes> = <TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<TApp["policies"]> = never, const TGuardsOpt extends LambderGuardsOption<TApp["guards"]> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<TApp["refusals"]> = never>(options: {
37
+ input: TInput;
38
+ output: TOutput;
39
+ } & {
40
+ /** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. A per-session policy needs one of the endpoint's guards to need a session. */
41
+ rateLimit?: TRateOpt;
42
+ /** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
43
+ idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
44
+ /**
45
+ * The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
46
+ * non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
47
+ * its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
48
+ */
49
+ refusals?: TRefusalsOpt;
50
+ /**
51
+ * Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
52
+ * body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
53
+ * compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
54
+ * cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
55
+ * both ends. A transport setting of this server's, not part of the API's contract.
56
+ */
57
+ compress?: boolean | "auto";
58
+ } & LambderRequirableGuardsField<TApp["guardsRequired"], TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<TApp["guards"], TApp["policies"], TGuardsOpt, TRateOpt, z.input<TInput>> & LambderPublicSessionPolicyCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["policies"], TRateOpt> & LambderSessionModeCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["sessions"]>,
59
+ /** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
60
+ handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => LambderMaybePromise<TAnswer>) => LambderApiDeclaration<LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, LambderApiModeOf<TApp["guards"], TGuardsOpt>, LambderGuardInputsOf<TApp["guards"], TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>>>;
61
+ /**
62
+ * Gathers endpoints into a named group: each is registered as `name.action`
63
+ * and called at `{apiPath}/{name}/{action}`, and every Lambder caller
64
+ * reaches it as `caller.name.action(input)`. A group's name and its actions
65
+ * are identifiers, and a group may not take a name a caller already answers
66
+ * for (see LAMBDER_RESERVED_GROUP_NAMES).
67
+ *
68
+ * A group declared across files takes each file's part:
69
+ * `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action two
70
+ * parts declare is refused, at compile time and at startup, rather than one
71
+ * silently replacing the other as an object spread would. Endpoints a
72
+ * module's builders declared may sit in any group: registering it adds
73
+ * their module.
74
+ */
75
+ export type LambderDefineApiGroup = <const TName extends string, const TParts extends readonly [LambderApiDeclarations, ...LambderApiDeclarations[]]>(name: TName & LambderGroupNameCheck<TName>, ...parts: TParts & LambderGroupPartsCheck<TParts> & {
76
+ [TIndex in keyof TParts]: LambderActionNamesCheck<TParts[TIndex]>;
77
+ }) => LambderApiGroup<TName, LambderMergedParts<TParts>>;
78
+ /**
79
+ * A group loaded on the first call to one of its endpoints:
80
+ * `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
81
+ * A cold start then parses none of it, nor anything only it imports, until
82
+ * a request calls it; the contract is the loaded group's, read off its type.
83
+ * The group loaded must carry the same name, which the type checks and
84
+ * loading checks again. `loadApiGroups()` loads every group, for a build
85
+ * step or a boot check that has to see them all.
86
+ *
87
+ * A module's lazyApiGroup carries the module, so its guards and policies
88
+ * are added when the group is registered, at startup, rather than when it
89
+ * loads; a group whose endpoints a module's builders declared is registered
90
+ * lazily with that module's.
91
+ */
92
+ export type LambderDefineLazyApiGroup = <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
93
+ /** The three builders, typed on one set of app types. */
94
+ export type LambderApiBuilders<TApp extends LambderAppTypes> = {
95
+ readonly defineApi: LambderDefineApi<TApp>;
96
+ readonly defineApiGroup: LambderDefineApiGroup;
97
+ readonly lazyApiGroup: LambderDefineLazyApiGroup;
98
+ };
99
+ /**
100
+ * The builders as values: the instance's own (module null), or a module's,
101
+ * whose endpoints and lazy groups carry the module for registration to add.
102
+ * Plain functions closing over nothing else, so an app may hand them out
103
+ * detached.
104
+ */
105
+ export declare const apiBuildersFor: <TApp extends LambderAppTypes>(module: LambderApiModuleDeclarations | null) => LambderApiBuilders<TApp>;
@@ -0,0 +1,12 @@
1
+ import { buildApiGroup, buildLazyApiGroup, } from "../api/LambderApiDeclarations.js";
2
+ /**
3
+ * The builders as values: the instance's own (module null), or a module's,
4
+ * whose endpoints and lazy groups carry the module for registration to add.
5
+ * Plain functions closing over nothing else, so an app may hand them out
6
+ * detached.
7
+ */
8
+ export const apiBuildersFor = (module) => ({
9
+ defineApi: ((options, handler) => module ? { kind: "lambderApi", options, handler, module } : { kind: "lambderApi", options, handler }),
10
+ defineApiGroup: ((name, ...parts) => buildApiGroup(name, parts)),
11
+ lazyApiGroup: ((name, load) => buildLazyApiGroup(name, load, module)),
12
+ });
@@ -0,0 +1,106 @@
1
+ import type { LambderAppTypes, LambderPlainAppTypes } from "../api/LambderApiDeclarations.js";
2
+ import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
3
+ import type { LambderGuardMetaMap } from "../api/LambderApiGuards.js";
4
+ import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
5
+ import type { LambderMergedNamedMaps, LambderNamedMapsOption } from "../shared/util/LambderNamedMaps.js";
6
+ import { type LambderDefineApi, type LambderDefineApiGroup, type LambderDefineLazyApiGroup } from "./LambderApiBuilders.js";
7
+ import type { LambderGivenOption, LambderGuardChecks, LambderGuardsMap, LambderRateLimitPoliciesMap, LambderRateLimitPolicyChecks } from "./LambderCreateOptions.js";
8
+ /**
9
+ * What defineApiModule() takes: the guards and the rate-limit policies a
10
+ * module declares for its own endpoints, each one map or a list of maps, as
11
+ * create() takes its own. The guards are pinned to the app's session type,
12
+ * as create()'s are.
13
+ */
14
+ export type LambderApiModuleOptions<TSessionData = any> = {
15
+ /** The module's guards, which its endpoints name beside the instance's: one map, or a list of maps merged in order. */
16
+ guards?: LambderNamedMapsOption<LambderGuardsMap<TSessionData>>;
17
+ /** The module's rate-limit policies, counted on the instance's limiter, which its endpoints name beside the instance's: one map, or a list of maps merged in order. */
18
+ rateLimitPolicies?: LambderNamedMapsOption<LambderRateLimitPoliciesMap>;
19
+ };
20
+ /** Every name a map, or each map of a list, declares. */
21
+ type LambderNamesDeclaredBy<TMaps> = TMaps extends readonly unknown[] ? {
22
+ [TIndex in keyof TMaps]: keyof TMaps[TIndex];
23
+ }[number] : keyof TMaps;
24
+ /** A module's map held to declaring names of its own: one the instance declares is a compile error, and the property name is the message. */
25
+ type LambderModuleNamesCheck<TMaps, TInstanceNames> = [
26
+ Extract<LambderNamesDeclaredBy<TMaps>, TInstanceNames>
27
+ ] extends [never] ? unknown : {
28
+ readonly "lambder: the instance declares these names already; a module declares names of its own": Extract<LambderNamesDeclaredBy<TMaps>, TInstanceNames>;
29
+ };
30
+ /**
31
+ * defineApiModule()'s options held to what create() holds its own to, one
32
+ * level down (each guard and each policy checked for surplus keys, a guard's
33
+ * refusal codes held to the vocabulary, a name two maps of a list declare
34
+ * refused), and to declaring no name the instance declares.
35
+ */
36
+ export type LambderApiModuleOptionChecks<TApp extends LambderAppTypes, TOptions extends LambderApiModuleOptions<TApp["session"]>> = {
37
+ guards?: LambderGuardChecks<TApp["session"], NonNullable<LambderGivenOption<TOptions, "guards">>, TApp["refusals"]> & LambderModuleNamesCheck<NonNullable<LambderGivenOption<TOptions, "guards">>, keyof TApp["guards"]>;
38
+ rateLimitPolicies?: LambderRateLimitPolicyChecks<NonNullable<LambderGivenOption<TOptions, "rateLimitPolicies">>> & LambderModuleNamesCheck<NonNullable<LambderGivenOption<TOptions, "rateLimitPolicies">>, keyof TApp["policies"]>;
39
+ } & LambderModuleDeclaresSomething<TOptions>;
40
+ /**
41
+ * A module declaring neither guards nor policies, refused as an empty guards
42
+ * option is: its builders would be the instance's own. The property name is
43
+ * the message.
44
+ */
45
+ type LambderModuleDeclaresSomething<TOptions> = [
46
+ LambderGivenOption<TOptions, "guards"> | LambderGivenOption<TOptions, "rateLimitPolicies">
47
+ ] extends [undefined] ? {
48
+ readonly "lambder: a module declares guards or rate-limit policies of its own; one with neither declares its endpoints with the instance's builders": never;
49
+ } : unknown;
50
+ /** One map from what a module option was given: a list merged, a lone map as it is, nothing for an option left out. */
51
+ type LambderModuleMap<TOption> = [TOption] extends [undefined] ? {} : LambderMergedNamedMaps<TOption>;
52
+ /**
53
+ * The app types a module's builders are typed on: the instance's, with the
54
+ * module's guards and rate-limit policies beside the instance's own. Its
55
+ * endpoints name either, and nothing else.
56
+ */
57
+ export type LambderModuleAppTypes<TApp extends LambderAppTypes, TGuards, TPolicies> = {
58
+ session: TApp["session"];
59
+ policies: TApp["policies"] & (LambderModuleMap<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {});
60
+ guards: TApp["guards"] & LambderGuardMetaMap<LambderModuleMap<TGuards>>;
61
+ idempotency: TApp["idempotency"];
62
+ guardsRequired: TApp["guardsRequired"];
63
+ sessions: TApp["sessions"];
64
+ refusals: TApp["refusals"];
65
+ refusalCodesRequired: TApp["refusalCodesRequired"];
66
+ };
67
+ /**
68
+ * A part of an app with guards and rate-limit policies of its own, for its
69
+ * own endpoints: the declaration builders typed on its instance's types with
70
+ * the module's guards and policies beside the instance's. Made by an
71
+ * instance's defineApiModule(); its endpoints are registered on the instance
72
+ * as any are (registerApiGroups), and their registration adds the module's
73
+ * guards and policies to the instance, refusing a name the instance or
74
+ * another module declares.
75
+ *
76
+ * So an app's core creates its instance knowing only its own policies, and
77
+ * each module brings its own: nothing has to import every module's guards
78
+ * before the instance can exist. Hand the builders out under the module's
79
+ * name, as an instance's are:
80
+ *
81
+ * ```typescript
82
+ * export const giftCardModule = storeApp.defineApiModule({ guards: { cardHolder }, rateLimitPolicies: { redeemPerCard } });
83
+ * export type GiftCardModule = typeof giftCardModule;
84
+ * export const defineApi: GiftCardModule["defineApi"] = giftCardModule.defineApi;
85
+ * ```
86
+ *
87
+ * @typeParam TApp - @internal The module's app types (LambderModuleAppTypes): the instance's, widened by the module's guards and policies (do not pass manually)
88
+ */
89
+ export declare class LambderApiModule<TApp extends LambderAppTypes = LambderPlainAppTypes> {
90
+ /**
91
+ * Type property for the module's app types, as an instance's AppTypes
92
+ * is: what LambderRenderContextOf<typeof module> reads, for a helper
93
+ * handed one of the module's handler contexts, its policies typed. It
94
+ * has no value.
95
+ */
96
+ readonly AppTypes: TApp;
97
+ /** Declares one of the module's endpoints: its guards and rate limits may name the instance's and the module's own. */
98
+ readonly defineApi: LambderDefineApi<TApp>;
99
+ /** Gathers endpoints into a named group, as the instance's does. */
100
+ readonly defineApiGroup: LambderDefineApiGroup;
101
+ /** A group of the module's loaded on the first call to it, carrying the module, so its guards and policies are added when the group is registered. */
102
+ readonly lazyApiGroup: LambderDefineLazyApiGroup;
103
+ /** Called by an instance's defineApiModule(), with the declarations it checked; an app does not construct one. */
104
+ constructor(declarations: LambderApiModuleDeclarations);
105
+ }
106
+ export {};
@@ -0,0 +1,45 @@
1
+ import { apiBuildersFor, } from "./LambderApiBuilders.js";
2
+ /**
3
+ * A part of an app with guards and rate-limit policies of its own, for its
4
+ * own endpoints: the declaration builders typed on its instance's types with
5
+ * the module's guards and policies beside the instance's. Made by an
6
+ * instance's defineApiModule(); its endpoints are registered on the instance
7
+ * as any are (registerApiGroups), and their registration adds the module's
8
+ * guards and policies to the instance, refusing a name the instance or
9
+ * another module declares.
10
+ *
11
+ * So an app's core creates its instance knowing only its own policies, and
12
+ * each module brings its own: nothing has to import every module's guards
13
+ * before the instance can exist. Hand the builders out under the module's
14
+ * name, as an instance's are:
15
+ *
16
+ * ```typescript
17
+ * export const giftCardModule = storeApp.defineApiModule({ guards: { cardHolder }, rateLimitPolicies: { redeemPerCard } });
18
+ * export type GiftCardModule = typeof giftCardModule;
19
+ * export const defineApi: GiftCardModule["defineApi"] = giftCardModule.defineApi;
20
+ * ```
21
+ *
22
+ * @typeParam TApp - @internal The module's app types (LambderModuleAppTypes): the instance's, widened by the module's guards and policies (do not pass manually)
23
+ */
24
+ export class LambderApiModule {
25
+ /**
26
+ * Type property for the module's app types, as an instance's AppTypes
27
+ * is: what LambderRenderContextOf<typeof module> reads, for a helper
28
+ * handed one of the module's handler contexts, its policies typed. It
29
+ * has no value.
30
+ */
31
+ AppTypes;
32
+ /** Declares one of the module's endpoints: its guards and rate limits may name the instance's and the module's own. */
33
+ defineApi;
34
+ /** Gathers endpoints into a named group, as the instance's does. */
35
+ defineApiGroup;
36
+ /** A group of the module's loaded on the first call to it, carrying the module, so its guards and policies are added when the group is registered. */
37
+ lazyApiGroup;
38
+ /** Called by an instance's defineApiModule(), with the declarations it checked; an app does not construct one. */
39
+ constructor(declarations) {
40
+ const builders = apiBuildersFor(declarations);
41
+ this.defineApi = builders.defineApi;
42
+ this.defineApiGroup = builders.defineApiGroup;
43
+ this.lazyApiGroup = builders.lazyApiGroup;
44
+ }
45
+ }