lambder 11.1.1 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +25 -15
  3. package/dist/api/LambderApiAnswer.d.ts +7 -0
  4. package/dist/api/LambderApiDeclarations.d.ts +194 -0
  5. package/dist/api/LambderApiDeclarations.js +52 -0
  6. package/dist/api/LambderApiDefinition.d.ts +1 -1
  7. package/dist/api/LambderApiEnvelope.js +4 -1
  8. package/dist/api/LambderApiGuards.d.ts +21 -22
  9. package/dist/api/LambderApiGuards.js +1 -1
  10. package/dist/api/LambderApiRateLimits.d.ts +19 -19
  11. package/dist/api/LambderApiRateLimits.js +1 -1
  12. package/dist/api/LambderApiRefusals.d.ts +22 -0
  13. package/dist/api/LambderApiRefusals.js +13 -0
  14. package/dist/api/LambderApiRequest.d.ts +15 -6
  15. package/dist/api/LambderApiRequest.js +8 -9
  16. package/dist/build/generateApiFiles.d.ts +60 -0
  17. package/dist/build/generateApiFiles.js +85 -0
  18. package/dist/build/generatedTables.d.ts +2 -1
  19. package/dist/build/generatedTables.js +1 -1
  20. package/dist/build.d.ts +4 -1
  21. package/dist/build.js +3 -1
  22. package/dist/client/LambderCaller.d.ts +36 -23
  23. package/dist/client/LambderCaller.js +88 -67
  24. package/dist/client/lambderFetchTransport.js +2 -1
  25. package/dist/client.d.ts +3 -1
  26. package/dist/client.js +2 -0
  27. package/dist/core/Lambder.d.ts +232 -92
  28. package/dist/core/Lambder.js +400 -103
  29. package/dist/core/LambderCallSummary.d.ts +43 -0
  30. package/dist/core/LambderCallSummary.js +10 -0
  31. package/dist/core/LambderContext.d.ts +15 -2
  32. package/dist/core/LambderContext.js +32 -9
  33. package/dist/core/LambderCreateOptions.d.ts +78 -64
  34. package/dist/core/LambderCreateOptions.js +39 -21
  35. package/dist/core/LambderInvocationScope.d.ts +25 -0
  36. package/dist/core/LambderInvocationScope.js +21 -0
  37. package/dist/core/LambderResponse.d.ts +11 -0
  38. package/dist/core/LambderResponse.js +12 -0
  39. package/dist/core/LambderResponseBuilder.js +5 -1
  40. package/dist/index.d.ts +13 -7
  41. package/dist/index.js +3 -1
  42. package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
  43. package/dist/invoke/LambderInvokeCaller.js +110 -94
  44. package/dist/invoke/LambderLambdaEvent.d.ts +2 -1
  45. package/dist/invoke/LambderLambdaEvent.js +8 -3
  46. package/dist/invoke/lambderHandlerTransport.js +2 -1
  47. package/dist/mock/LambderMockApp.d.ts +39 -33
  48. package/dist/mock/LambderMockApp.js +66 -55
  49. package/dist/mock/LambderMockCreateOptions.d.ts +8 -10
  50. package/dist/mock/LambderMockTypes.d.ts +44 -7
  51. package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
  52. package/dist/mock/lambderMockInvokeTransport.js +15 -7
  53. package/dist/mock/lambderMockMswHandler.js +8 -4
  54. package/dist/mock.d.ts +2 -2
  55. package/dist/session/LambderSessionManager.d.ts +46 -3
  56. package/dist/session/LambderSessionManager.js +62 -11
  57. package/dist/shared/LambderI18n.js +49 -4
  58. package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
  59. package/dist/shared/contracts/LambderRateLimiter.js +2 -2
  60. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  61. package/dist/shared/transport/LambderApiTransport.js +2 -3
  62. package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
  63. package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
  64. package/dist/shared/util/LambderInvocationScope.js +5 -0
  65. package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
  66. package/dist/shared/util/LambderNamedMaps.js +19 -0
  67. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  68. package/dist/shared/wire/LambderApiContract.d.ts +4 -18
  69. package/dist/shared/wire/LambderApiContract.js +1 -1
  70. package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
  71. package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
  72. package/dist/shared/wire/LambderApiNames.d.ts +55 -0
  73. package/dist/shared/wire/LambderApiNames.js +81 -0
  74. package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
  75. package/dist/shared/wire/LambderCallOutcome.js +47 -0
  76. package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
  77. package/dist/shared/wire/LambderInvokeApiId.js +10 -0
  78. package/dist/shared/wire/LambderUploadSchemas.js +2 -1
  79. package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
  80. package/dist/stores/LambderDdbRateLimiter.js +64 -45
  81. package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
  82. package/dist/stores/LambderMemoryRateLimiter.js +20 -15
  83. package/dist/testing/LambderTestApp.d.ts +18 -5
  84. package/dist/testing/LambderTestApp.js +21 -5
  85. package/dist/testing/LambderTestVisitor.d.ts +16 -8
  86. package/dist/testing/LambderTestVisitor.js +33 -25
  87. package/package.json +1 -1
@@ -72,9 +72,11 @@ export type LambderGuardRefusals<TRefusals extends readonly string[] = readonly
72
72
  *
73
73
  * Orthogonally, a guard may also:
74
74
  *
75
- * - declare `session: true`: the guard needs ctx.session, so it is only
76
- * declarable on addSessionApi (compile error and startup assert on public
77
- * APIs) and its handler receives the session-typed context.
75
+ * - declare `session: true`: the guard needs ctx.session, so it makes every
76
+ * endpoint declaring it a session endpoint (the session is read before the
77
+ * guards run, a call without one is answered sessionExpired, and the
78
+ * endpoint's handler receives the session-typed context), and it receives
79
+ * the session-typed context itself.
78
80
  * - take a PARAMETER: annotate a 3rd handler argument
79
81
  * (`(ctx, payload, param: YourType) => ...`) and APIs pass the value in
80
82
  * their declaration: `guards: { yourGuard: paramValue }`. The value is
@@ -272,19 +274,16 @@ export type LambderGuardMetaMap<TGuards> = {
272
274
  [K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
273
275
  };
274
276
  /**
275
- * Guard names an API may declare: every guard the instance holds, session
276
- * guards only on session APIs. Whether the API's input carries the fields an
277
- * apiInput guard reads is asked of the names it declared, once that input is
278
- * known, by LambderGuardNamesInputLacks.
277
+ * Guard names an API may declare: every guard the instance holds, a session
278
+ * guard included, which makes the API a session one (LambderApiModeOf).
279
+ * Whether the API's input carries the fields an apiInput guard reads is
280
+ * asked of the names it declared, once that input is known, by
281
+ * LambderGuardNamesInputLacks.
279
282
  */
280
- export type LambderAllowedGuardNames<TGuards, TIncludeSession extends boolean = true> = {
281
- [K in keyof TGuards]: TGuards[K] extends {
282
- session: true;
283
- } ? (TIncludeSession extends true ? K : never) : K;
284
- }[keyof TGuards] & string;
283
+ export type LambderAllowedGuardNames<TGuards> = keyof TGuards & string;
285
284
  /**
286
285
  * The apiInput guards among `TNames` whose fields the API's payload does not
287
- * carry. addApi and addSessionApi ask this once the API's input is known, of
286
+ * carry. defineApi asks this once the API's input is known, of
288
287
  * the names its guards option holds; see LambderPayloadSliceCheck.
289
288
  *
290
289
  * The payload is compared whole, not member by member: a bare `TPayload
@@ -298,14 +297,14 @@ export type LambderGuardNamesInputLacks<TGuards, TNames, TPayload> = {
298
297
  } ? ([TPayload] extends [R] ? never : K) : never;
299
298
  }[TNames & keyof TGuards] & string;
300
299
  /** The allowed guard names whose handler takes no param (usable in the string/array forms). */
301
- export type LambderParamlessGuardNames<TGuards, TIncludeSession extends boolean> = {
302
- [K in LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards]: TGuards[K] extends {
300
+ export type LambderParamlessGuardNames<TGuards> = {
301
+ [K in LambderAllowedGuardNames<TGuards> & keyof TGuards]: TGuards[K] extends {
303
302
  param: undefined;
304
303
  } ? K & string : never;
305
- }[LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards];
304
+ }[LambderAllowedGuardNames<TGuards> & keyof TGuards];
306
305
  /** The map form's full shape: every declarable guard name, each carrying its own param type. */
307
- type LambderGuardsMap<TGuards, TIncludeSession extends boolean> = {
308
- readonly [K in LambderAllowedGuardNames<TGuards, TIncludeSession> & keyof TGuards]?: TGuards[K] extends {
306
+ type LambderGuardsMap<TGuards> = {
307
+ readonly [K in LambderAllowedGuardNames<TGuards> & keyof TGuards]?: TGuards[K] extends {
309
308
  param: undefined;
310
309
  } ? true : TGuards[K] extends {
311
310
  param: infer P;
@@ -324,10 +323,10 @@ type LambderGuardsMap<TGuards, TIncludeSession extends boolean> = {
324
323
  * compiler builds it once per instance rather than once per API; see
325
324
  * LambderPayloadSliceCheck for why, and for where the input is asked.
326
325
  */
327
- export type LambderGuardsOption<TGuards, TIncludeSession extends boolean> = LambderParamlessGuardNames<TGuards, TIncludeSession> | readonly [
328
- LambderParamlessGuardNames<TGuards, TIncludeSession>,
329
- ...LambderParamlessGuardNames<TGuards, TIncludeSession>[]
330
- ] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards, TIncludeSession>>;
326
+ export type LambderGuardsOption<TGuards> = LambderParamlessGuardNames<TGuards> | readonly [
327
+ LambderParamlessGuardNames<TGuards>,
328
+ ...LambderParamlessGuardNames<TGuards>[]
329
+ ] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards>>;
331
330
  /**
332
331
  * The typed ctx.guardData an API's handler sees: declared guards that return
333
332
  * a value, keyed by name. Check-only (void) guards never appear.
@@ -101,7 +101,7 @@ export class LambderApiGuardsEngine {
101
101
  throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation.`);
102
102
  }
103
103
  if (guardDef.session && mode !== "session") {
104
- throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true), which requires addSessionApi.`);
104
+ throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true) but is registered as public. An endpoint declaring a session guard is a session endpoint.`);
105
105
  }
106
106
  }
107
107
  }
@@ -116,7 +116,7 @@ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy
116
116
  export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
117
117
  /** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
118
118
  limiter: LambderRateLimiter;
119
- /** Named policies referenced (typed) from addApi/addSessionApi. */
119
+ /** Named policies referenced (typed) from defineApi. */
120
120
  policies: TPolicies;
121
121
  /**
122
122
  * Let the request through when the limiter itself fails (the table is
@@ -144,23 +144,23 @@ export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderA
144
144
  * without one, and a union holding undefined for a policy typed as the
145
145
  * general LambderApiRateLimitPolicyConfig, where only registration can tell.
146
146
  */
147
- type LambderPolicyPerOf<TPolicy> = "per" extends keyof TPolicy ? TPolicy["per" & keyof TPolicy] : undefined;
147
+ export type LambderPolicyPerOf<TPolicy> = "per" extends keyof TPolicy ? TPolicy["per" & keyof TPolicy] : undefined;
148
148
  /**
149
- * Policy names an API may reference: session-keyed policies only on session
150
- * APIs, and never a policy without `per`, whose key only the code that
151
- * charges it knows. A policy whose type does not settle its `per` is allowed
152
- * here and checked at registration. Whether the API's input carries the
153
- * fields an apiInput-keyed policy reads is asked of the names it declared,
154
- * once that input is known, by LambderPolicyNamesInputLacks.
149
+ * Policy names an API may reference: never a policy without `per`, whose key
150
+ * only the code that charges it knows. A policy whose type does not settle
151
+ * its `per` is allowed here and checked at registration. A session-keyed
152
+ * policy is allowed here too, and refused of an API none of whose guards
153
+ * needs a session by LambderPublicSessionPolicyCheck, once its guards are
154
+ * known. Whether the API's input carries the fields an apiInput-keyed policy
155
+ * reads is asked of the names it declared, once that input is known, by
156
+ * LambderPolicyNamesInputLacks.
155
157
  */
156
- export type LambderAllowedPolicyNames<TPolicies, TIncludeSession extends boolean> = {
157
- [K in keyof TPolicies]: [
158
- LambderPolicyPerOf<TPolicies[K]>
159
- ] extends [undefined] ? never : [LambderPolicyPerOf<TPolicies[K]>] extends ["session"] ? (TIncludeSession extends true ? K : never) : K;
158
+ export type LambderAllowedPolicyNames<TPolicies> = {
159
+ [K in keyof TPolicies]: [LambderPolicyPerOf<TPolicies[K]>] extends [undefined] ? never : K;
160
160
  }[keyof TPolicies] & string;
161
161
  /**
162
162
  * The apiInput-keyed policies among `TNames` whose key fields the API's
163
- * payload does not carry. addApi and addSessionApi ask this once the API's
163
+ * payload does not carry. defineApi asks this once the API's
164
164
  * input is known, of the names its rateLimit option holds; see
165
165
  * LambderPayloadSliceCheck.
166
166
  *
@@ -235,8 +235,8 @@ type LambderRateLimitOverrideFor<TPolicy> = TPolicy extends {
235
235
  budget: "perPolicy";
236
236
  } ? Pick<LambderRateLimitOverride, "refusal"> : LambderRateLimitOverride;
237
237
  /** The map form's full shape: every referable policy name, each carrying its own override. */
238
- type LambderRateLimitMap<TPolicies, TIncludeSession extends boolean> = {
239
- readonly [K in LambderAllowedPolicyNames<TPolicies, TIncludeSession> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
238
+ type LambderRateLimitMap<TPolicies> = {
239
+ readonly [K in LambderAllowedPolicyNames<TPolicies> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
240
240
  };
241
241
  /**
242
242
  * The per-API `rateLimit` option: one policy name, a non-empty ordered list
@@ -251,10 +251,10 @@ type LambderRateLimitMap<TPolicies, TIncludeSession extends boolean> = {
251
251
  * It depends on the instance alone, never on the API's input, as
252
252
  * LambderGuardsOption does; see LambderPayloadSliceCheck.
253
253
  */
254
- export type LambderRateLimitOption<TPolicies, TIncludeSession extends boolean> = LambderAllowedPolicyNames<TPolicies, TIncludeSession> | readonly [
255
- LambderAllowedPolicyNames<TPolicies, TIncludeSession>,
256
- ...LambderAllowedPolicyNames<TPolicies, TIncludeSession>[]
257
- ] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies, TIncludeSession>>;
254
+ export type LambderRateLimitOption<TPolicies> = LambderAllowedPolicyNames<TPolicies> | readonly [
255
+ LambderAllowedPolicyNames<TPolicies>,
256
+ ...LambderAllowedPolicyNames<TPolicies>[]
257
+ ] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies>>;
258
258
  /**
259
259
  * When in a call a policy is checked.
260
260
  *
@@ -209,7 +209,7 @@ export class LambderApiRateLimitsEngine {
209
209
  throw new Error(`Lambder: API "${apiName}" references rate-limit policy "${name}", which declares no per: its key is the one a handler passes to ctx.rateLimit("${name}", key), so the request alone cannot be counted against it.`);
210
210
  }
211
211
  if (policy.per === "session" && mode !== "session") {
212
- throw new Error(`Lambder: API "${apiName}" uses rate-limit policy "${name}" (per "session"), which requires addSessionApi.`);
212
+ throw new Error(`Lambder: API "${apiName}" uses rate-limit policy "${name}" (per "session"), which counts per session, and none of its guards needs a session.`);
213
213
  }
214
214
  if (override)
215
215
  assertWindowLimits(`API "${apiName}" override of rate-limit policy "${name}"`, override);
@@ -3,6 +3,7 @@ import type { LambderJsonOf } from "../shared/wire/LambderApiContract.js";
3
3
  import type { LambderRefusalStatusCode } from "../shared/wire/LambderHttpStatus.js";
4
4
  import type { LambderNoExtraKeys } from "../shared/util/LambderTypeUtilities.js";
5
5
  import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
6
+ import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderNoRepeatedNames } from "../shared/util/LambderNamedMaps.js";
6
7
  /**
7
8
  * One code of an app's refusal vocabulary: the schema of the data it carries,
8
9
  * when it carries data, and how every refusal with the code leaves the
@@ -69,6 +70,27 @@ export type LambderRefusalVocabularyChecks<TVocabulary> = {
69
70
  readonly "lambder: a refusal code may not start with lambder/, the prefix of the framework's own codes": never;
70
71
  } : LambderNoExtraKeys<TVocabulary[TCode], LambderRefusalDeclaration> & LambderRefusalDataCheck<TVocabulary[TCode]>;
71
72
  };
73
+ /**
74
+ * The vocabulary as declareRefusals() takes it: one map of codes, or a list of
75
+ * them, so each part of an app declares its own codes beside its APIs. A
76
+ * code two maps declare is refused.
77
+ */
78
+ export type LambderRefusalVocabularyOption = LambderNamedMapsOption<LambderRefusalVocabulary>;
79
+ /** The one vocabulary a declareRefusals() option declares: a list's maps merged, a lone map as it is. */
80
+ export type LambderMergedRefusalVocabulary<TOption> = LambderMergedNamedMaps<TOption> extends infer TMerged extends LambderRefusalVocabulary ? TMerged : never;
81
+ /** A declareRefusals() option checked code by code, in each map of a list, and a list declaring no code twice. */
82
+ export type LambderRefusalVocabularyOptionChecks<TOption> = TOption extends readonly unknown[] ? {
83
+ [TIndex in keyof TOption]: LambderRefusalVocabularyChecks<TOption[TIndex]>;
84
+ } & LambderNoRepeatedNames<TOption> : LambderRefusalVocabularyChecks<TOption>;
85
+ /**
86
+ * A declareRefusals() option as the one vocabulary it declares, checked
87
+ * (see readRefusalVocabulary) and keyed. `declarer` names the init in the
88
+ * errors.
89
+ */
90
+ export declare const declaredRefusalVocabulary: (option: LambderRefusalVocabularyOption, declarer: string) => {
91
+ refusals: LambderRefusalVocabulary;
92
+ vocabulary: Map<string, LambderRefusalDeclaration>;
93
+ };
72
94
  /** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
73
95
  export declare const allowedRefusalOf: (declaration: LambderRefusalDeclaration) => LambderApiAllowedRefusal;
74
96
  /**
@@ -1,5 +1,18 @@
1
1
  import { LambderApiRefusal, isLambderRefusalCode } from "../shared/wire/LambderApiRefusal.js";
2
2
  import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
3
+ import { mergeNamedMaps } from "../shared/util/LambderNamedMaps.js";
4
+ /**
5
+ * A declareRefusals() option as the one vocabulary it declares, checked
6
+ * (see readRefusalVocabulary) and keyed. `declarer` names the init in the
7
+ * errors.
8
+ */
9
+ export const declaredRefusalVocabulary = (option, declarer) => {
10
+ if (option === null || typeof option !== "object") {
11
+ throw new Error(`${declarer}: declareRefusals() takes the vocabulary, an object of codes or a list of them.`);
12
+ }
13
+ const refusals = mergeNamedMaps(option, "refusal code", "declareRefusals()");
14
+ return { refusals, vocabulary: readRefusalVocabulary(refusals) };
15
+ };
3
16
  /** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
4
17
  export const allowedRefusalOf = (declaration) => {
5
18
  const leaves = {
@@ -18,7 +18,15 @@ export declare const lowercaseHeaderNames: (headers: Record<string, string | und
18
18
  * transport request, and from here on nothing in the pipeline knows which.
19
19
  */
20
20
  export type LambderApiRequest = {
21
+ /** The endpoint called: `group.action`, read off the path the call was posted to (`{apiPath}/{group}/{action}`). */
21
22
  apiName: string;
23
+ /**
24
+ * Set on a call posted to apiPath itself with the endpoint named in the
25
+ * body, the way callers did before endpoints had paths: such a caller is a
26
+ * page loaded from an older build, and it is answered that its version
27
+ * expired, which reloads it.
28
+ */
29
+ retiredPath?: true;
22
30
  /** The caller's apiVersion, informational; null when it sent none. */
23
31
  version: string | null;
24
32
  /** The signature the caller carries for this endpoint (see LambderApiSignatureMap), for the signature gate; null when it sent none. */
@@ -79,13 +87,14 @@ export type LambderApiRequestInfo = {
79
87
  */
80
88
  export declare const isApiCallContentType: (lowercasedHeaders: Record<string, string | undefined>) => boolean;
81
89
  /**
82
- * Reads the posted envelope into a request. Null when the body carries no
83
- * apiName, which is how the server tells an API call from a route with a
84
- * JSON body. Everything is taken as posted: a malformed idempotencyKey or
85
- * guardInputs value is the engines' to refuse, with the client-facing
86
- * message they already give.
90
+ * Reads the posted envelope of a call to `apiName` into a request; the name
91
+ * comes from where the call was posted, never from the body. Everything is
92
+ * taken as posted: a malformed idempotencyKey or guardInputs value is the
93
+ * engines' to refuse, with the client-facing message they already give.
87
94
  */
88
- export declare const readApiEnvelope: (post: Record<string, unknown> | null | undefined, info: LambderApiRequestInfo) => LambderApiRequest | null;
95
+ export declare const readApiEnvelope: (post: Record<string, unknown> | null | undefined, info: LambderApiRequestInfo, apiName: string, flags?: {
96
+ retiredPath?: true;
97
+ }) => LambderApiRequest;
89
98
  /** Outcome of restoring a compressed request payload; the message is client-facing. */
90
99
  export type LambderRestorePayloadResult = {
91
100
  ok: true;
@@ -43,20 +43,19 @@ export const lowercaseHeaderNames = (headers) => {
43
43
  */
44
44
  export const isApiCallContentType = (lowercasedHeaders) => (lowercasedHeaders["content-type"] ?? "").split(";")[0].trim().toLowerCase() === "application/json";
45
45
  /**
46
- * Reads the posted envelope into a request. Null when the body carries no
47
- * apiName, which is how the server tells an API call from a route with a
48
- * JSON body. Everything is taken as posted: a malformed idempotencyKey or
49
- * guardInputs value is the engines' to refuse, with the client-facing
50
- * message they already give.
46
+ * Reads the posted envelope of a call to `apiName` into a request; the name
47
+ * comes from where the call was posted, never from the body. Everything is
48
+ * taken as posted: a malformed idempotencyKey or guardInputs value is the
49
+ * engines' to refuse, with the client-facing message they already give.
51
50
  */
52
- export const readApiEnvelope = (post, info) => {
53
- if (!post || typeof post.apiName !== "string" || !post.apiName)
54
- return null;
51
+ export const readApiEnvelope = (post, info, apiName, flags = {}) => {
52
+ post ??= {};
55
53
  const hasGzip = post[COMPRESSED_PAYLOAD_GZ_FIELD] !== undefined;
56
54
  const hasBrotli = post[COMPRESSED_PAYLOAD_BR_FIELD] !== undefined;
57
55
  const guardInputs = post.guardInputs;
58
56
  return {
59
- apiName: post.apiName,
57
+ apiName,
58
+ ...(flags.retiredPath ? { retiredPath: true } : {}),
60
59
  version: typeof post.version === "string" ? post.version : null,
61
60
  signature: typeof post.signature === "string" ? post.signature : null,
62
61
  token: typeof post.token === "string" ? post.token : "",
@@ -0,0 +1,60 @@
1
+ import type { LambderModuleLocation } from "./moduleLocation.js";
2
+ import { type LambderApiContractFileOptions } from "./writeApiContract.js";
3
+ import { type LambderApiSignatureFileOptions } from "./writeApiSignatures.js";
4
+ import { type LambderApiOptionsFileOptions } from "./writeApiOptions.js";
5
+ import { type LambderApiGuardParamsFileOptions } from "./writeApiGuardParams.js";
6
+ /** What a file's writer takes beyond where the instance is, which the app gives once for all its files. */
7
+ type LambderAppFile<TOptions> = Omit<TOptions, "module" | "exportName" | "tsconfig" | "check">;
8
+ /** One app: where its instance is, and which files it is written to. Each file is optional. */
9
+ export type LambderApiFilesApp = {
10
+ /** The module that exports the instance, usually the server's entry: a path relative to the working directory, or a file URL. */
11
+ module: LambderModuleLocation;
12
+ /** The export that holds the instance. Default: "default". */
13
+ exportName?: string;
14
+ /** The tsconfig.json the module compiles under, which the contract is read through. Default: the nearest one at or above the module. */
15
+ tsconfig?: string;
16
+ /** The contract a client compiles against, as plain types (writeApiContract). */
17
+ contract?: LambderAppFile<LambderApiContractFileOptions>;
18
+ /** The signature file both sides ship (writeApiSignatures). */
19
+ signatures?: LambderAppFile<LambderApiSignatureFileOptions>;
20
+ /** The declared options as plain data, for a mock and tests (writeApiOptions). */
21
+ options?: LambderAppFile<LambderApiOptionsFileOptions>;
22
+ /** One file per guard whose parameters a browser needs (writeApiGuardParams). */
23
+ guardParams?: readonly LambderAppFile<LambderApiGuardParamsFileOptions>[];
24
+ };
25
+ /** The files generateApiFiles writes: every app's, by a name the output lines use. */
26
+ export type LambderApiFilesConfig = {
27
+ apps: Record<string, LambderApiFilesApp>;
28
+ };
29
+ export type LambderApiFilesResult = {
30
+ /** False when a check found a file stale, or a writer failed. */
31
+ ok: boolean;
32
+ /** What happened, per app and file, as lines to print. */
33
+ lines: string[];
34
+ };
35
+ /**
36
+ * Writes every file the config names, or with `check` writes nothing and
37
+ * answers whether each one on disk is current. Paths are relative to the
38
+ * working directory, as each writer takes them.
39
+ *
40
+ * ```ts
41
+ * import { generateApiFiles } from "lambder/build";
42
+ *
43
+ * const result = await generateApiFiles({
44
+ * apps: {
45
+ * server: {
46
+ * module: "backend/index.ts",
47
+ * exportName: "lambder",
48
+ * contract: { file: "shared/generated/apiContract.generated.ts" },
49
+ * signatures: { file: "shared/generated/apiSignatures.generated.ts" },
50
+ * },
51
+ * },
52
+ * }, { check: process.argv.includes("--check") });
53
+ * console.log(result.lines.join("\n"));
54
+ * process.exit(result.ok ? 0 : 1);
55
+ * ```
56
+ */
57
+ export declare const generateApiFiles: (config: LambderApiFilesConfig, options?: {
58
+ check?: boolean;
59
+ }) => Promise<LambderApiFilesResult>;
60
+ export {};
@@ -0,0 +1,85 @@
1
+ import { writeApiContract } from "./writeApiContract.js";
2
+ import { writeApiSignatures } from "./writeApiSignatures.js";
3
+ import { writeApiOptions } from "./writeApiOptions.js";
4
+ import { writeApiGuardParams } from "./writeApiGuardParams.js";
5
+ const APP_KEYS = ["module", "exportName", "tsconfig", "contract", "signatures", "options", "guardParams"];
6
+ const FILE_KINDS = ["contract", "signatures", "options", "guardParams"];
7
+ /** A config that names apps, each with its module and at least one file, and no key the generator does not read. */
8
+ function assertConfig(config) {
9
+ const apps = config?.apps;
10
+ if (apps === null || typeof apps !== "object" || Array.isArray(apps) || Object.keys(apps).length === 0) {
11
+ throw new Error("generateApiFiles: the config names no apps. Give `apps`, one entry per Lambder instance: { module, exportName, and the files it is written to }.");
12
+ }
13
+ for (const [name, app] of Object.entries(apps)) {
14
+ if (app === null || typeof app !== "object")
15
+ throw new Error(`generateApiFiles: the app "${name}" is not an object.`);
16
+ const unknownKeys = Object.keys(app).filter((key) => !APP_KEYS.includes(key));
17
+ if (unknownKeys.length)
18
+ throw new Error(`generateApiFiles: the app "${name}" has ${unknownKeys.map((key) => `"${key}"`).join(", ")}, which the generator does not read (it reads ${APP_KEYS.join(", ")}).`);
19
+ if (!app.module)
20
+ throw new Error(`generateApiFiles: the app "${name}" names no module to read its instance from.`);
21
+ if (!FILE_KINDS.some((kind) => app[kind] !== undefined)) {
22
+ throw new Error(`generateApiFiles: the app "${name}" is written to no file; give it a contract, signatures, options or guardParams.`);
23
+ }
24
+ }
25
+ }
26
+ /** A writer's run as lines: its own on success or a stale check, the reason when it threw. */
27
+ const settle = async (label, write) => {
28
+ try {
29
+ return await write();
30
+ }
31
+ catch (err) {
32
+ const reasons = [];
33
+ for (let cause = err; cause instanceof Error && reasons.length < 4; cause = cause.cause)
34
+ reasons.push(cause.message);
35
+ return { ok: false, lines: [`✗ ${label} failed: ${reasons.join(": ") || String(err)}`] };
36
+ }
37
+ };
38
+ /**
39
+ * Writes every file the config names, or with `check` writes nothing and
40
+ * answers whether each one on disk is current. Paths are relative to the
41
+ * working directory, as each writer takes them.
42
+ *
43
+ * ```ts
44
+ * import { generateApiFiles } from "lambder/build";
45
+ *
46
+ * const result = await generateApiFiles({
47
+ * apps: {
48
+ * server: {
49
+ * module: "backend/index.ts",
50
+ * exportName: "lambder",
51
+ * contract: { file: "shared/generated/apiContract.generated.ts" },
52
+ * signatures: { file: "shared/generated/apiSignatures.generated.ts" },
53
+ * },
54
+ * },
55
+ * }, { check: process.argv.includes("--check") });
56
+ * console.log(result.lines.join("\n"));
57
+ * process.exit(result.ok ? 0 : 1);
58
+ * ```
59
+ */
60
+ export const generateApiFiles = async (config, options = {}) => {
61
+ assertConfig(config);
62
+ const check = options.check ?? false;
63
+ const apps = Object.entries(config.apps);
64
+ const runs = [];
65
+ // The contracts first: they compile the app's sources and run none of
66
+ // them, so a module that fails to load still gets its contract checked.
67
+ for (const [name, app] of apps) {
68
+ if (!app.contract)
69
+ continue;
70
+ runs.push(await settle(`${name}: the contract`, () => writeApiContract({ ...app.contract, module: app.module, exportName: app.exportName, tsconfig: app.tsconfig, check })));
71
+ }
72
+ // Then what is read off the instance itself, which importing the module
73
+ // builds once for all three.
74
+ for (const [name, app] of apps) {
75
+ const instance = { module: app.module, exportName: app.exportName, check };
76
+ if (app.signatures)
77
+ runs.push(await settle(`${name}: the signatures`, () => writeApiSignatures({ ...app.signatures, ...instance })));
78
+ if (app.options)
79
+ runs.push(await settle(`${name}: the options`, () => writeApiOptions({ ...app.options, ...instance })));
80
+ for (const guardFile of app.guardParams ?? []) {
81
+ runs.push(await settle(`${name}: the "${guardFile.guard}" guard parameters`, () => writeApiGuardParams({ ...guardFile, ...instance })));
82
+ }
83
+ }
84
+ return { ok: runs.every((run) => run.ok), lines: runs.flatMap((run) => run.lines) };
85
+ };
@@ -2,7 +2,8 @@ import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEnt
2
2
  import { type LambderModuleLocation } from "./moduleLocation.js";
3
3
  /** What the generators read the options from: a Lambder instance, or anything else that reports them the same way. */
4
4
  export type LambderApiOptionsSource = {
5
- apiOptionEntries(): LambderApiOptionEntries;
5
+ /** Asynchronous, since a Lambder instance loads its lazy groups before it can report every endpoint. */
6
+ apiOptionEntries(): Promise<LambderApiOptionEntries> | LambderApiOptionEntries;
6
7
  };
7
8
  /** Which names of one table moved: entries that changed, entries the file did not have, entries the file had and the instance no longer reports. */
8
9
  export type LambderNameChanges = {
@@ -16,7 +16,7 @@ export const loadApiOptionEntries = async (location, generator) => {
16
16
  if (typeof source?.apiOptionEntries !== "function") {
17
17
  throw new Error(`${moduleUrl} has no export "${exportName}" that reports API options: name the export holding the instance in exportName`);
18
18
  }
19
- return source.apiOptionEntries();
19
+ return await source.apiOptionEntries();
20
20
  };
21
21
  /** One table of a generated file, read back as data, or null for a file that does not hold it as written. */
22
22
  export const readGeneratedTable = (contents, exportName) => {
package/dist/build.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * ships: the signature file both sides read, the declared options as plain
6
6
  * data, and one guard's parameters as the least a browser needs of them, all
7
7
  * from the app's own instance; and the contract a client compiles against,
8
- * from the server's sources. Node-only and imported by nothing else
8
+ * from the server's sources; and generateApiFiles, every one of them for
9
+ * every app a script names, in one call. Node-only and imported by nothing else
9
10
  * in the package, so no deployment or bundle carries it.
10
11
  */
11
12
  export { writeApiSignatures } from "./build/writeApiSignatures.js";
@@ -17,4 +18,6 @@ export type { LambderApiOptionsFileOptions, LambderApiOptionsFileResult, } from
17
18
  export { writeApiGuardParams } from "./build/writeApiGuardParams.js";
18
19
  export type { LambderApiGuardParamsFileOptions, LambderApiGuardParamsFileResult, } from "./build/writeApiGuardParams.js";
19
20
  export type { LambderApiOptionsSource, LambderNameChanges } from "./build/generatedTables.js";
21
+ export { generateApiFiles } from "./build/generateApiFiles.js";
22
+ export type { LambderApiFilesConfig, LambderApiFilesApp, LambderApiFilesResult } from "./build/generateApiFiles.js";
20
23
  export type { LambderModuleLocation } from "./build/moduleLocation.js";
package/dist/build.js CHANGED
@@ -5,10 +5,12 @@
5
5
  * ships: the signature file both sides read, the declared options as plain
6
6
  * data, and one guard's parameters as the least a browser needs of them, all
7
7
  * from the app's own instance; and the contract a client compiles against,
8
- * from the server's sources. Node-only and imported by nothing else
8
+ * from the server's sources; and generateApiFiles, every one of them for
9
+ * every app a script names, in one call. Node-only and imported by nothing else
9
10
  * in the package, so no deployment or bundle carries it.
10
11
  */
11
12
  export { writeApiSignatures } from "./build/writeApiSignatures.js";
12
13
  export { writeApiContract } from "./build/writeApiContract.js";
13
14
  export { writeApiOptions } from "./build/writeApiOptions.js";
14
15
  export { writeApiGuardParams } from "./build/writeApiGuardParams.js";
16
+ export { generateApiFiles } from "./build/generateApiFiles.js";
@@ -5,6 +5,7 @@ import { type LambderApiOutcome, type LambderValidationError } from '../shared/w
5
5
  import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from '../shared/wire/LambderCallOptions.js';
6
6
  import { type LambderApiTransport } from '../shared/transport/LambderApiTransport.js';
7
7
  import { type LambderApiSignatureMap } from '../shared/wire/LambderApiSignatureMap.js';
8
+ import { type LambderContractActionOf, type LambderContractGroupsOf, type LambderContractNamesInGroup } from '../shared/wire/LambderApiGroupCalls.js';
8
9
  export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
9
10
  export type { LambderProvidedGuardInputs, LambderGuardInputsProvider } from '../shared/wire/LambderCallOptions.js';
10
11
  /** A handler told that something happened, with nothing to hand it. */
@@ -110,40 +111,23 @@ type LambderCallerBaseOptions<TMessage extends LambderUncheckedRefusalMessage =
110
111
  /** Constructor options: the base options plus guardInputsProvider, mandatory once TProvided names guards. */
111
112
  export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions<LambderContractAnyRefusalMessage<TContract>> & LambderGuardInputsProviderOption<TContract, TProvided>;
112
113
  /**
114
+ * The caller itself, before the groups: what LambderCaller is, less the
115
+ * endpoints it reaches by group (LambderCallerGroupCalls), which the
116
+ * constructor adds.
117
+ *
113
118
  * @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
114
119
  * @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
115
120
  */
116
- export default class LambderCaller<TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never> {
117
- private apiPath;
118
- private apiVersion?;
119
- private apiSignatures?;
120
- private timeoutMs?;
121
+ declare class LambderCallerCore<TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never> {
122
+ #private;
121
123
  /** The calls currently in flight, in the order they started. */
122
124
  fetchTrackerList: FetchTracker[];
123
125
  /** Whether any call is in flight. Derived from the list, so the two cannot drift apart. */
124
126
  get isLoading(): boolean;
125
- private versionExpiredHandler?;
126
- private sessionExpiredHandler?;
127
- private refusalHandler?;
128
- private notAuthorizedHandler?;
129
- private errorHandler?;
130
- private apiInputValidationErrorHandler?;
131
- private logListHandler?;
132
- private fetchStartedHandler?;
133
- private fetchEndedHandler?;
134
- private guardInputsProvider?;
135
- private sessionTokenCookieKey;
136
- private sessionCsrfCookieKey;
137
- private sessionCookieDomain?;
138
- private requestCompression;
139
- private transport;
140
127
  constructor(options: LambderCallerOptions<TContract, TProvidedGuards>);
141
128
  setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
142
129
  /** Replaces how calls reach the server: a mock runtime, an in-process handler, a decorated transport. */
143
130
  setTransport(transport: LambderApiTransport): this;
144
- private clearSessionCookies;
145
- /** One call, one outcome. Never throws; every failure path resolves to { ok: false }. */
146
- private dispatch;
147
131
  /**
148
132
  * Full-fidelity call: resolves to a discriminated LambderApiOutcome
149
133
  * instead of collapsing every failure to undefined. Never throws. A
@@ -164,3 +148,32 @@ export default class LambderCaller<TContract extends LambderApiContractShape = a
164
148
  */
165
149
  api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TApiName>>>): Promise<LambderContractOutputOf<TContract, TApiName> | undefined>;
166
150
  }
151
+ /**
152
+ * One endpoint as a caller hands it out on its group: called, it is `api`
153
+ * for that endpoint (the output, or undefined on a failure); `.outcome` is
154
+ * `apiOutcome` (the full outcome, never throwing).
155
+ */
156
+ export type LambderCallerEndpoint<TContract, TName extends keyof TContract & string, TProvidedGuards extends string> = {
157
+ (...args: LambderCallArgs<TContract, TName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TName>>>): Promise<LambderContractOutputOf<TContract, TName> | undefined>;
158
+ outcome(...args: LambderCallArgs<TContract, TName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TName>>>): Promise<LambderApiOutcome<LambderContractOutputOf<TContract, TName>, LambderContractRefusalMessage<TContract, TName>>>;
159
+ };
160
+ /** Every endpoint of a contract, by group: `caller.orders.place(input)`. */
161
+ export type LambderCallerGroupCalls<TContract, TProvidedGuards extends string> = {
162
+ readonly [TGroup in LambderContractGroupsOf<TContract>]: {
163
+ readonly [TName in LambderContractNamesInGroup<TContract, TGroup> as LambderContractActionOf<TName>]: LambderCallerEndpoint<TContract, TName, TProvidedGuards>;
164
+ };
165
+ };
166
+ /**
167
+ * A typed client of a Lambder app: `caller.orders.place(input)` for the
168
+ * endpoint `orders.place`, `caller.orders.place.outcome(input)` for its full
169
+ * outcome, and `caller.api("orders.place", input)` for code that has the
170
+ * name as a value.
171
+ */
172
+ type LambderCaller<TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never> = LambderCallerCore<TContract, TProvidedGuards> & LambderCallerGroupCalls<TContract, TProvidedGuards>;
173
+ /** The caller's own members, without the groups: what a wrapper of a caller (the test visitor) types its `api` and `apiOutcome` by. */
174
+ export type LambderCallerMembers<TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never> = LambderCallerCore<TContract, TProvidedGuards>;
175
+ declare const LambderCaller: {
176
+ new <TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never>(options: LambderCallerOptions<TContract, TProvidedGuards>): LambderCaller<TContract, TProvidedGuards>;
177
+ readonly prototype: LambderCallerCore<any, any>;
178
+ };
179
+ export default LambderCaller;