lambder 14.0.1 → 15.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 (163) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +14 -17
  3. package/dist/api/LambderApiDeclarations.d.ts +11 -5
  4. package/dist/api/LambderApiDefinition.d.ts +2 -1
  5. package/dist/api/LambderApiEnvelope.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.js +8 -2
  7. package/dist/api/LambderApiGuards.d.ts +44 -5
  8. package/dist/api/LambderApiGuards.js +27 -0
  9. package/dist/api/LambderApiIdempotency.d.ts +14 -7
  10. package/dist/api/LambderApiIdempotency.js +39 -27
  11. package/dist/api/LambderApiPipeline.d.ts +47 -13
  12. package/dist/api/LambderApiPipeline.js +62 -20
  13. package/dist/api/LambderApiRateLimits.d.ts +28 -21
  14. package/dist/api/LambderApiRateLimits.js +55 -35
  15. package/dist/api/LambderApiRefusals.d.ts +26 -10
  16. package/dist/api/LambderApiRefusals.js +48 -15
  17. package/dist/api/LambderApiRequest.d.ts +21 -1
  18. package/dist/api/LambderApiRequest.js +31 -2
  19. package/dist/api/LambderApiSignature.d.ts +5 -5
  20. package/dist/api/LambderApiSignature.js +7 -5
  21. package/dist/build/checkApiRefusals.d.ts +59 -0
  22. package/dist/build/checkApiRefusals.js +442 -0
  23. package/dist/build/contractPrinterProcess.d.ts +11 -0
  24. package/dist/build/contractPrinterProcess.js +17 -0
  25. package/dist/build/generateApiFiles.d.ts +26 -5
  26. package/dist/build/generateApiFiles.js +68 -7
  27. package/dist/build/loadTypeScriptCompiler.d.ts +10 -0
  28. package/dist/build/loadTypeScriptCompiler.js +20 -0
  29. package/dist/build/writeApiContract.js +21 -15
  30. package/dist/build/writeApiSchemas.d.ts +109 -0
  31. package/dist/build/writeApiSchemas.js +308 -0
  32. package/dist/build.d.ts +8 -3
  33. package/dist/build.js +6 -2
  34. package/dist/client/LambderCaller.d.ts +25 -11
  35. package/dist/client/LambderCaller.js +57 -49
  36. package/dist/client/LambderUploadRunner.d.ts +55 -27
  37. package/dist/client/LambderUploadRunner.js +89 -34
  38. package/dist/client.d.ts +6 -6
  39. package/dist/client.js +2 -2
  40. package/dist/core/Lambder.d.ts +37 -26
  41. package/dist/core/Lambder.js +89 -32
  42. package/dist/core/LambderCallSummary.d.ts +1 -1
  43. package/dist/core/LambderContext.d.ts +59 -7
  44. package/dist/core/LambderContext.js +31 -15
  45. package/dist/core/LambderCrashHandling.d.ts +5 -0
  46. package/dist/core/LambderCrashHandling.js +11 -1
  47. package/dist/core/LambderCreateOptions.d.ts +28 -17
  48. package/dist/core/LambderCreateOptions.js +14 -7
  49. package/dist/core/LambderResolver.d.ts +3 -2
  50. package/dist/core/LambderResponseBuilder.d.ts +20 -9
  51. package/dist/core/LambderResponseBuilder.js +39 -8
  52. package/dist/core/LambderTemplatingEngine.d.ts +24 -6
  53. package/dist/core/LambderTemplatingEngine.js +23 -3
  54. package/dist/index.d.ts +15 -12
  55. package/dist/index.js +7 -5
  56. package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
  57. package/dist/invoke/LambderInvokeCaller.js +47 -9
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
  59. package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
  60. package/dist/mock/LambderMockAdapterDoor.js +10 -0
  61. package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
  62. package/dist/mock/LambderMockApiSchemas.js +150 -0
  63. package/dist/mock/LambderMockApp.d.ts +56 -48
  64. package/dist/mock/LambderMockApp.js +107 -62
  65. package/dist/mock/LambderMockBrowserCookies.js +18 -5
  66. package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
  67. package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
  68. package/dist/mock/LambderMockFailureInjector.js +12 -8
  69. package/dist/mock/LambderMockTypes.d.ts +8 -4
  70. package/dist/mock/lambderMockInvokeTransport.js +2 -9
  71. package/dist/mock/lambderMockMswHandler.d.ts +19 -13
  72. package/dist/mock/lambderMockMswHandler.js +25 -22
  73. package/dist/mock.d.ts +1 -0
  74. package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
  75. package/dist/secrets/LambderOneShotSecrets.js +6 -36
  76. package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
  77. package/dist/secrets/LambderPasswordHasher.js +168 -0
  78. package/dist/session/LambderSessionController.d.ts +37 -4
  79. package/dist/session/LambderSessionController.js +36 -6
  80. package/dist/session/LambderSessionManager.d.ts +33 -3
  81. package/dist/session/LambderSessionManager.js +55 -16
  82. package/dist/shared/LambderHtml.d.ts +23 -4
  83. package/dist/shared/LambderHtml.js +17 -5
  84. package/dist/shared/LambderI18n.d.ts +106 -16
  85. package/dist/shared/LambderI18n.js +259 -74
  86. package/dist/shared/contracts/LambderCache.d.ts +14 -1
  87. package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
  88. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  89. package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
  90. package/dist/shared/util/LambderBackoffTimer.js +40 -12
  91. package/dist/shared/util/LambderCallAbort.d.ts +22 -6
  92. package/dist/shared/util/LambderCallAbort.js +37 -9
  93. package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
  94. package/dist/shared/util/LambderErrorReportMark.js +22 -0
  95. package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
  96. package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
  97. package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
  98. package/dist/shared/util/LambderSignedClaims.js +54 -0
  99. package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
  100. package/dist/shared/util/LambderSwappableInstances.js +54 -0
  101. package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
  102. package/dist/shared/util/LambderTestingDoors.js +6 -2
  103. package/dist/shared/util/LambderTextDigest.d.ts +5 -5
  104. package/dist/shared/util/LambderTextDigest.js +5 -5
  105. package/dist/shared/wire/LambderApiContract.d.ts +1 -1
  106. package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
  107. package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
  108. package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
  109. package/dist/shared/wire/LambderApiOutcome.d.ts +21 -12
  110. package/dist/shared/wire/LambderApiOutcome.js +1 -1
  111. package/dist/shared/wire/LambderApiRefusal.d.ts +41 -14
  112. package/dist/shared/wire/LambderApiRefusal.js +3 -3
  113. package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
  114. package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
  115. package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
  116. package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
  117. package/dist/shared/wire/LambderCallOutcome.js +3 -2
  118. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
  119. package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
  120. package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
  121. package/dist/shared/wire/LambderInvokeApiId.js +3 -3
  122. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
  123. package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
  124. package/dist/stores/LambderCacheValues.d.ts +2 -5
  125. package/dist/stores/LambderDdbCache.d.ts +15 -9
  126. package/dist/stores/LambderDdbCache.js +25 -6
  127. package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
  128. package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
  129. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
  130. package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
  131. package/dist/stores/LambderDdbSdk.d.ts +59 -29
  132. package/dist/stores/LambderDdbSdk.js +116 -44
  133. package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
  134. package/dist/stores/LambderDdbSessionStore.js +133 -68
  135. package/dist/stores/LambderMemoryCache.d.ts +2 -2
  136. package/dist/stores/LambderS3FileSource.d.ts +3 -2
  137. package/dist/stores/LambderS3FileSource.js +11 -13
  138. package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
  139. package/dist/stores/LambderS3UploadBucket.js +23 -0
  140. package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
  141. package/dist/stores/LambderStoredText.d.ts +26 -0
  142. package/dist/stores/LambderStoredText.js +25 -0
  143. package/dist/testing/LambderConformanceRunner.d.ts +9 -2
  144. package/dist/testing/LambderConformanceRunner.js +9 -2
  145. package/dist/testing/LambderTestApp.d.ts +65 -7
  146. package/dist/testing/LambderTestApp.js +101 -6
  147. package/dist/testing/LambderTestVisitor.d.ts +43 -12
  148. package/dist/testing/LambderTestVisitor.js +35 -11
  149. package/dist/testing/bootLambdaPackage.d.ts +105 -0
  150. package/dist/testing/bootLambdaPackage.js +265 -0
  151. package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
  152. package/dist/testing/lambdaPackageBootProcess.js +130 -0
  153. package/dist/testing/lambderCacheConformance.d.ts +26 -0
  154. package/dist/testing/lambderCacheConformance.js +287 -0
  155. package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
  156. package/dist/testing/lambderCacheStorageConformance.js +159 -0
  157. package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
  158. package/dist/testing/lambderSessionStoreConformance.js +3 -3
  159. package/dist/testing.d.ts +7 -1
  160. package/dist/testing.js +5 -0
  161. package/package.json +1 -6
  162. package/dist/shared/util/boundKeyField.d.ts +0 -20
  163. package/dist/shared/util/boundKeyField.js +0 -34
@@ -1,13 +1,22 @@
1
1
  import { checkUploadRule, } from "../shared/contracts/LambderUploadBucket.js";
2
2
  import { LambderBackoffTimer } from "../shared/util/LambderBackoffTimer.js";
3
3
  import { sha256Base64Of } from "../shared/util/LambderTextDigest.js";
4
- /** How an upload failed: `reason` for a screen to word, the underlying error as `cause`. */
4
+ /**
5
+ * How an upload failed: `reason` for a screen to word, the underlying error
6
+ * as `cause`, and the failed outcome of the app's own call as `callFailure`
7
+ * when that call is what ended the upload, so a screen words a refusal by its
8
+ * code (`callFailure.refusal.code`) as it would on the call itself.
9
+ */
5
10
  export class LambderUploadError extends Error {
6
11
  reason;
12
+ /** The ticket or confirm call's failure outcome, when the upload ended on one. */
13
+ callFailure;
7
14
  constructor(reason, options = {}) {
8
15
  super(options.detail ? `${reason}: ${options.detail}` : reason, { cause: options.cause });
9
16
  this.name = "LambderUploadError";
10
17
  this.reason = reason;
18
+ if (options.callFailure)
19
+ this.callFailure = options.callFailure;
11
20
  }
12
21
  }
13
22
  /**
@@ -19,19 +28,38 @@ export class LambderUploadError extends Error {
19
28
  const TICKET_RENEWAL_LIMIT = 2;
20
29
  /** S3's refusals that are the connection's or the service's fault rather than the file's, which its own SDK retries too. */
21
30
  const TRANSIENT_STORAGE_CODES = new Set(["RequestTimeout", "SlowDown", "InternalError", "ServiceUnavailable"]);
31
+ /**
32
+ * The ticket and confirm call failures that got no usable answer, so the same
33
+ * call may well pass on another try: no answer at all, the call's own
34
+ * timeout, a 5xx. Every other failure is the endpoint's answer (a refusal, a
35
+ * rejected input, an expired session), and the same call would get it again.
36
+ */
37
+ const RETRIED_CALL_REASONS = new Set(["network", "timeout", "server"]);
38
+ /** One line on what a failed call said, for the message of the upload error it ends in. */
39
+ const describeCallFailure = (failure) => {
40
+ const said = failure.refusal !== undefined ? failure.refusal.content
41
+ : "error" in failure ? failure.error.message
42
+ : "";
43
+ return said ? `${failure.reason}: ${said}` : failure.reason;
44
+ };
22
45
  export class LambderUploadRunner {
23
46
  options;
24
- attempts;
47
+ /** How many times a step is tried again after its first try: the storageRetry option's attempts, less that first one. */
48
+ retries;
25
49
  /** The ladder one upload's waits climb; each upload() builds a timer of its own from it, so two uploads never share a count. */
26
50
  backoff;
27
51
  stallTimeoutMs;
28
52
  constructor(options) {
29
53
  this.options = options;
30
- this.attempts = Math.max(1, options.storageRetry?.attempts ?? 4);
54
+ this.retries = Math.max(1, options.storageRetry?.attempts ?? 4) - 1;
31
55
  const baseDelayMs = options.storageRetry?.baseDelayMs ?? 1_000;
32
56
  const maxDelayMs = options.storageRetry?.maxDelayMs ?? 15_000;
33
57
  // A longest wait below the shortest is every wait at the shortest.
34
58
  this.backoff = { baseMs: baseDelayMs, maxMs: Math.max(baseDelayMs, maxDelayMs) };
59
+ // Each upload climbs a timer of its own; one built here checks the
60
+ // ladder where the runner is configured, so a storageRetry the timer
61
+ // refuses (a baseDelayMs of 0) fails now rather than on the first upload.
62
+ new LambderBackoffTimer(this.backoff);
35
63
  this.stallTimeoutMs = options.stallTimeoutMs ?? 60_000;
36
64
  }
37
65
  /** For a file input's `accept`, so the picker only offers what the rule takes. */
@@ -56,11 +84,11 @@ export class LambderUploadRunner {
56
84
  if (rejection)
57
85
  throw new LambderUploadError(rejection);
58
86
  const report = (phase, sentBytes = 0) => onProgress?.({ phase, sentBytes, totalBytes: file.size });
59
- const stopIfCancelled = () => {
87
+ const stopIfAborted = () => {
60
88
  if (signal?.aborted)
61
- throw new LambderUploadError("cancelled");
89
+ throw new LambderUploadError("aborted");
62
90
  };
63
- stopIfCancelled();
91
+ stopIfAborted();
64
92
  report("hashing");
65
93
  const fileFacts = {
66
94
  fileName: file.name,
@@ -73,28 +101,64 @@ export class LambderUploadRunner {
73
101
  throw new LambderUploadError("fileUnreadable", { cause });
74
102
  }))),
75
103
  };
76
- stopIfCancelled();
77
- const requestTicket = async () => {
78
- report("requesting");
79
- try {
80
- return await this.options.requestTicket(fileFacts, { signal });
81
- }
82
- catch (cause) {
83
- throw new LambderUploadError(signal?.aborted ? "cancelled" : "ticketRefused", { cause });
104
+ stopIfAborted();
105
+ // One timer for the whole upload, the app's calls and storage alike.
106
+ // A step that succeeds resets it, so each step has every attempt and
107
+ // its first wait is the shortest again. Every retry so far waited on
108
+ // the timer once, so its count is the retries the step has spent.
109
+ const backoff = new LambderBackoffTimer(this.backoff);
110
+ const waitToRetry = async (lastFailure) => {
111
+ if (backoff.retries >= this.retries)
112
+ throw new LambderUploadError("networkFailed", lastFailure);
113
+ // The wait rejects only for the signal: nothing else cancels it.
114
+ await backoff.wait(signal).catch((cause) => {
115
+ throw new LambderUploadError("aborted", { cause });
116
+ });
117
+ };
118
+ /**
119
+ * One of the app's calls through to its output. A failure with no
120
+ * usable answer is tried again on the timer, the same request each
121
+ * time (so a confirm asks about the ticket it was issued); any other
122
+ * failure ends the upload as `refused`, and one after the upload's
123
+ * signal aborted as the abort. A throw is the app's own code failing
124
+ * rather than the call, which no retry cures.
125
+ */
126
+ const callApp = async (call, refused) => {
127
+ for (;;) {
128
+ stopIfAborted();
129
+ let outcome;
130
+ try {
131
+ outcome = await call();
132
+ }
133
+ catch (cause) {
134
+ throw new LambderUploadError(signal?.aborted ? "aborted" : refused, { cause });
135
+ }
136
+ if (outcome.ok) {
137
+ backoff.reset();
138
+ return outcome.payload;
139
+ }
140
+ const ended = { callFailure: outcome, detail: describeCallFailure(outcome), ...("error" in outcome ? { cause: outcome.error } : {}) };
141
+ if (outcome.reason === "aborted" || signal?.aborted)
142
+ throw new LambderUploadError("aborted", ended);
143
+ if (!RETRIED_CALL_REASONS.has(outcome.reason))
144
+ throw new LambderUploadError(refused, ended);
145
+ await waitToRetry(ended);
84
146
  }
85
147
  };
148
+ const requestTicket = () => {
149
+ report("requesting");
150
+ return callApp(() => this.options.requestTicket(fileFacts, { signal }), "ticketRefused");
151
+ };
86
152
  let issued = await requestTicket();
87
- const backoff = new LambderBackoffTimer(this.backoff);
88
- let failedAttempts = 0;
89
153
  let ticketRenewals = 0;
90
154
  for (;;) {
91
- stopIfCancelled();
155
+ stopIfAborted();
92
156
  report("uploading");
93
157
  const outcome = await this.send(issued.ticket, file, signal, (sentBytes) => report("uploading", sentBytes));
94
158
  if (outcome.kind === "stored")
95
159
  break;
96
- if (outcome.kind === "cancelled")
97
- throw new LambderUploadError("cancelled");
160
+ if (outcome.kind === "aborted")
161
+ throw new LambderUploadError("aborted");
98
162
  if (outcome.kind === "rejected") {
99
163
  // An expired ticket needs no wait and costs no attempt, only a
100
164
  // new ticket; any other refusal is the file's, and final.
@@ -105,20 +169,11 @@ export class LambderUploadRunner {
105
169
  }
106
170
  // The ticket is kept through a network retry, so a flaky
107
171
  // connection does not leave the app a record per attempt.
108
- if (++failedAttempts >= this.attempts)
109
- throw new LambderUploadError("networkFailed");
110
- // The wait rejects only for the signal: nothing else cancels it.
111
- await backoff.wait(signal).catch((cause) => {
112
- throw new LambderUploadError("cancelled", { cause });
113
- });
172
+ await waitToRetry({});
114
173
  }
174
+ backoff.reset();
115
175
  report("confirming", file.size);
116
- try {
117
- return await this.options.confirmUpload(issued.reference, { signal });
118
- }
119
- catch (cause) {
120
- throw new LambderUploadError(signal?.aborted ? "cancelled" : "confirmRefused", { cause });
121
- }
176
+ return await callApp(() => this.options.confirmUpload(issued, { signal }), "confirmRefused");
122
177
  }
123
178
  /** Forgets a confirmed upload through the app's endpoint, when it declared one. */
124
179
  async discard(receipt) {
@@ -127,7 +182,7 @@ export class LambderUploadRunner {
127
182
  /** One upload of the file to storage, in the ticket's form. Never throws: every ending is an outcome. */
128
183
  send(ticket, file, signal, onSent) {
129
184
  if (signal?.aborted)
130
- return Promise.resolve({ kind: "cancelled" });
185
+ return Promise.resolve({ kind: "aborted" });
131
186
  let request;
132
187
  if (ticket.method === "PUT") {
133
188
  request = { method: "PUT", url: ticket.uploadUrl, body: file, headers: ticket.headers };
@@ -176,7 +231,7 @@ const sendWithXhr = ({ method, url, body, headers }, fileBytes, stallTimeoutMs,
176
231
  settle(rejectedOutcome(request.status, request.responseText));
177
232
  };
178
233
  request.onerror = () => settle({ kind: "unreachable" });
179
- request.onabort = () => settle(stalled ? { kind: "unreachable" } : { kind: "cancelled" });
234
+ request.onabort = () => settle(stalled ? { kind: "unreachable" } : { kind: "aborted" });
180
235
  signal?.addEventListener("abort", cancel, { once: true });
181
236
  watchForStall();
182
237
  try {
@@ -196,7 +251,7 @@ const sendWithFetch = async ({ method, url, body, headers }, fileBytes, signal,
196
251
  response = await fetch(url, { method, body, headers, signal });
197
252
  }
198
253
  catch {
199
- return signal?.aborted ? { kind: "cancelled" } : { kind: "unreachable" };
254
+ return signal?.aborted ? { kind: "aborted" } : { kind: "unreachable" };
200
255
  }
201
256
  if (response.ok) {
202
257
  onSent(fileBytes);
package/dist/client.d.ts CHANGED
@@ -19,17 +19,17 @@ export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_L
19
19
  export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignatureMap.js";
20
20
  export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
21
21
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
22
- export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
22
+ export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiFailure, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
23
23
  export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderLogListHandler, LambderCallerEndpoint, LambderCallerGroupCalls, LambderCallerMembers, } from "./client/LambderCaller.js";
24
24
  export { apiCallPath, splitApiName, LAMBDER_RESERVED_GROUP_NAMES, LAMBDER_RESERVED_ACTION_NAMES } from "./shared/wire/LambderApiNames.js";
25
25
  export type { LambderContractGroupsOf, LambderContractNamesInGroup, LambderContractActionOf } from "./shared/wire/LambderApiGroupCalls.js";
26
26
  export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
27
27
  export type { LambderIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
28
28
  export { LambderApiRefusal, isLambderApiRefusal, isLambderRefusalCode, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
29
- export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderPlainRefusalMessage, LambderUncheckedRefusalMessage, LambderRefusalCode, LambderRefuseOptions, LambderDeclaredRefuseOptions, LambderDeclaredRefuse, } from "./shared/wire/LambderApiRefusal.js";
29
+ export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderPlainRefusalMessage, LambderRateLimitRefusalData, LambderUncheckedRefusalMessage, LambderRefusalCode, LambderRefuseOptions, LambderDeclaredRefuseOptions, LambderDeclaredRefuse, } from "./shared/wire/LambderApiRefusal.js";
30
30
  export { isObjectPayload } from "./shared/wire/LambderObjectPayload.js";
31
31
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiSuccessEnvelope, LambderApiRefusalEnvelope, LambderRefusalEnvelopeFields, LambderApiRefusalConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderContractRefusalsOf, LambderContractRefusalMessage, LambderContractRefusals, LambderContractAnyRefusalMessage, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
32
- export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
32
+ export { apiGuardParam, apiGuardParamExportName, apisWithGuard } from "./shared/wire/LambderApiOptionEntries.js";
33
33
  export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, LambderApiGuardParam, } from "./shared/wire/LambderApiOptionEntries.js";
34
34
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
35
35
  export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
@@ -37,13 +37,13 @@ export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_
37
37
  export type { LambderCompressedGzipPayload, LambderCompressedBrotliPayload, LambderRequestCompressionOption, LambderRequestCompressionSettings, } from "./shared/wire/LambderRequestPayload.js";
38
38
  export { resolveCompressionOption } from "./shared/wire/LambderCompressionOption.js";
39
39
  export type { LambderCompressionOption, LambderCompressionSettingsBase } from "./shared/wire/LambderCompressionOption.js";
40
- export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
40
+ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue, type LambderJsonScriptOptions } from "./shared/LambderHtml.js";
41
41
  export { createLambderI18n } from "./shared/LambderI18n.js";
42
- export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
42
+ export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nReadonlyInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nDictionaryEntry, LambderI18nPluralEntry, LambderI18nPluralCategory, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
43
43
  export type { LambderHttpStatusCode, LambderRefusalStatusCode } from "./shared/wire/LambderHttpStatus.js";
44
44
  export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
45
45
  export type { LambderBackoffTimerOptions } from "./shared/util/LambderBackoffTimer.js";
46
- export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
46
+ export { LambderSignedClaims, keyedDigest, randomSecret, randomCode } from "./shared/util/LambderSignedClaims.js";
47
47
  export type { LambderSignedClaimsOptions } from "./shared/util/LambderSignedClaims.js";
48
48
  export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
49
49
  export { LambderUploadRunner, LambderUploadError } from "./client/LambderUploadRunner.js";
package/dist/client.js CHANGED
@@ -28,7 +28,7 @@ export { LambderApiRefusal, isLambderApiRefusal, isLambderRefusalCode, refuse, r
28
28
  export { isObjectPayload } from "./shared/wire/LambderObjectPayload.js";
29
29
  // The server's declared options as plain data (the generated options module's
30
30
  // entry types) and the readers a client derives its own facts from.
31
- export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
31
+ export { apiGuardParam, apiGuardParamExportName, apisWithGuard } from "./shared/wire/LambderApiOptionEntries.js";
32
32
  // A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
33
33
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
34
34
  // Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
@@ -45,7 +45,7 @@ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
45
45
  // Signed claims tokens, for the isomorphic code that verifies them where a
46
46
  // server's secret is at hand (an edge Worker, a shared backend package);
47
47
  // never in a page, which holds no secret to verify with.
48
- export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
48
+ export { LambderSignedClaims, keyedDigest, randomSecret, randomCode } from "./shared/util/LambderSignedClaims.js";
49
49
  export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
50
50
  // Direct uploads: the runner that takes a file from the browser straight to
51
51
  // storage, and the vocabulary it shares with the server's bucket.
@@ -78,11 +78,12 @@ type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt
78
78
  * ```
79
79
  */
80
80
  export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes, _TContract extends Record<string, any> = {}> {
81
- apiPath: string;
81
+ /** Where API calls go: `{apiPath}/{group}/{action}`. */
82
+ readonly apiPath: string;
82
83
  /** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
83
- apiVersion: null | string;
84
+ readonly apiVersion: null | string;
84
85
  /** The instance's file reader (source + caches), or null without the files option. */
85
- files: LambderFiles | null;
86
+ readonly files: LambderFiles | null;
86
87
  /**
87
88
  * Type property for extracting the API contract: every registered API's
88
89
  * input, output, mode and declared options, as a client calls it.
@@ -125,7 +126,6 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
125
126
  /** Every API's refusals option as written, for apiOptionEntries(); its definition holds the resolved set. */
126
127
  private readonly refusalOptions;
127
128
  private hookList;
128
- private createdHooks;
129
129
  private initPromise;
130
130
  private globalErrorHandler;
131
131
  private routeFallbackHandler;
@@ -411,6 +411,17 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
411
411
  * endpoint and never moves when registrations are reordered.
412
412
  */
413
413
  apiOptionEntries(): Promise<LambderApiOptionEntries>;
414
+ /**
415
+ * Every registered API's input and output schemas by name, sorted by
416
+ * name: what writeApiSchemas (lambder/build) writes as JSON Schema to a
417
+ * module the mock validates its calls against. A build-time view, as
418
+ * apiSignatureEntries() is: it comes off the server instance, which a
419
+ * generator imports and a client never does.
420
+ */
421
+ apiSchemaEntries(): Promise<Record<string, {
422
+ input: z.ZodType;
423
+ output: z.ZodType;
424
+ }>>;
414
425
  getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder;
415
426
  private getResolver;
416
427
  getHandler(): LambderHandler;
@@ -510,8 +521,8 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
510
521
  * The answer to a request that needed a session and has none, whether it
511
522
  * never had one or it ended while the request held it: an API call gets
512
523
  * the protocol's sessionExpired envelope, as the pipeline gives a session
513
- * API, and anything else the setSessionExpiredRouteHandler answer, a 401
514
- * by default.
524
+ * API, and anything else the setSessionExpiredRouteHandler answer, a
525
+ * plain-text 401 by default, as the framework's own 404 and 500 are.
515
526
  */
516
527
  private sessionMissingResponse;
517
528
  /**
@@ -560,21 +571,30 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
560
571
  private runApi;
561
572
  /**
562
573
  * A thrown LambderApiRefusal (from a hook, say) as the structured API
563
- * envelope: the core's one mapping, after the same check the pipeline
564
- * applies, against the endpoint the call names, its lazy group loaded if
565
- * the refusal came before the call reached it. A name no API is
566
- * registered under declares no code, so only an uncoded or a framework
567
- * refusal goes out for it.
574
+ * envelope: the pipeline's own answer to a refusal (answerRefusal), so
575
+ * it is checked against the endpoint the call names, its lazy group
576
+ * loaded if the refusal came before the call reached it, and one saying
577
+ * the session is over ends it. A name no API is registered under
578
+ * declares no code, so only an uncoded or a framework refusal goes out
579
+ * for it.
568
580
  */
569
581
  private apiErrorResponse;
570
582
  }
583
+ /** The options create() takes: the constructor's, less the two declareRefusals() supplies. */
584
+ export type LambderInitCreateOptions<TSessionData> = Omit<LambderCreateOptions<TSessionData>, "refusals" | "requireRefusalCodes">;
571
585
  /**
572
- * The canonical way to create an instance: fix the session data type first,
573
- * then create with the full configuration in one declaration. The policy,
574
- * guard and idempotency types are inferred from the options, so the instance
575
- * is born fully typed, and the endpoints declared with its defineApi are
576
- * typed against it. There are no ordering rules, and nothing can be
577
- * half-configured.
586
+ * The entry point of an app, and the canonical way to create an instance:
587
+ * fix the session data type first, then create with the full configuration
588
+ * in one declaration. The policy, guard and idempotency types are inferred
589
+ * from the options, so the instance is born fully typed, and the endpoints
590
+ * declared with its defineApi are typed against it. There are no ordering
591
+ * rules, and nothing can be half-configured.
592
+ *
593
+ * Beside create() it hands out the builders that share the session data
594
+ * type (guard, rateLimitKey, refuse). `declareRefusals()` binds the app's
595
+ * refusal vocabulary too, so a guard's ctx.refuse and the init's own refuse
596
+ * are typed to it before any instance exists, and create() gives it to the
597
+ * instance for every API's refusals option to name codes from.
578
598
  *
579
599
  * ```typescript
580
600
  * // app.ts (imports no api modules, so modules can import from it)
@@ -603,15 +623,6 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
603
623
  * session type in the first call lets the second infer everything else.
604
624
  * `new Lambder(options)` serves untyped or session-data-free instances.
605
625
  */
606
- /** The options create() takes: the constructor's, less the two declareRefusals() supplies. */
607
- export type LambderInitCreateOptions<TSessionData> = Omit<LambderCreateOptions<TSessionData>, "refusals" | "requireRefusalCodes">;
608
- /**
609
- * The entry point of an app: binds the session data type, and hands out the
610
- * builders and create() that share it. `declareRefusals()` binds the app's
611
- * refusal vocabulary too, so a guard's ctx.refuse and the init's own refuse
612
- * are typed to it before any instance exists, and create() gives it to the
613
- * instance for every API's refusals option to name codes from.
614
- */
615
626
  export declare const initLambder: <TSessionData = any>() => {
616
627
  /**
617
628
  * Declares the app's refusal vocabulary: every code once, with the schema
@@ -13,12 +13,12 @@ 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 { checkedRefusal, declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
16
+ import { 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";
20
20
  import { assertPlainData } from "../shared/util/assertPlainData.js";
21
- import { apiNotFoundAnswer, envelopeAnswer, refusalAnswer, sessionExpiredAnswer, successEnvelope, versionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
21
+ import { apiNotFoundAnswer, envelopeAnswer, sessionExpiredAnswer, successEnvelope, versionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
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";
@@ -80,6 +80,12 @@ export default class Lambder {
80
80
  // Construction
81
81
  // Everything an instance is, fixed before the first registration.
82
82
  // =====================================================================
83
+ // Fixed at construction, because what was built from them keeps its own
84
+ // copy: the pipeline and the crash handling stamp the apiVersion they
85
+ // were given, servePublicFiles serves through the reader it was given,
86
+ // and the callers of an instance post to the apiPath they were built
87
+ // with. A later write would leave the instance disagreeing with itself.
88
+ /** Where API calls go: `{apiPath}/{group}/{action}`. */
83
89
  apiPath;
84
90
  /** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
85
91
  apiVersion;
@@ -126,8 +132,7 @@ export default class Lambder {
126
132
  refusalVocabulary;
127
133
  /** Every API's refusals option as written, for apiOptionEntries(); its definition holds the resolved set. */
128
134
  refusalOptions = new Map();
129
- hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
130
- createdHooks = [];
135
+ hookList = { "created": [], "beforeRender": [], "afterRender": [], "fallback": [] };
131
136
  initPromise = null;
132
137
  globalErrorHandler = null;
133
138
  routeFallbackHandler = null;
@@ -211,6 +216,7 @@ export default class Lambder {
211
216
  rateLimits: options.rateLimits,
212
217
  guards: options.guards,
213
218
  idempotency: options.idempotency,
219
+ atRestSecret: session?.sessionSalt,
214
220
  });
215
221
  this.trustedClientIpHeaders = options.trustedClientIpHeaders ?? [];
216
222
  this.trustedHostHeaders = options.trustedHostHeaders ?? [];
@@ -539,14 +545,11 @@ export default class Lambder {
539
545
  this.apiHandlers.set(name, { output: api.output, compress: api.compress, handler: api.handler });
540
546
  }
541
547
  addHook(hookEvent, hookFn, priority = 0) {
542
- if (hookEvent === "created") {
543
- // Runs once, lazily, before the first request or event is handled.
544
- this.createdHooks.push(hookFn);
545
- }
546
- else {
547
- this.hookList[hookEvent].push({ priority, hookFn });
548
- this.hookList[hookEvent].sort((a, b) => a.priority - b.priority);
549
- }
548
+ // In priority order, lower first; the sort is stable, so hooks of one
549
+ // priority run in the order they were added. A created hook runs once,
550
+ // lazily, before the first request or event is handled.
551
+ this.hookList[hookEvent].push({ priority, hookFn });
552
+ this.hookList[hookEvent].sort((a, b) => a.priority - b.priority);
550
553
  return this;
551
554
  }
552
555
  addAction(filter, actionFn) {
@@ -723,6 +726,23 @@ export default class Lambder {
723
726
  }
724
727
  return { apis, rateLimitPolicies, guards };
725
728
  }
729
+ /**
730
+ * Every registered API's input and output schemas by name, sorted by
731
+ * name: what writeApiSchemas (lambder/build) writes as JSON Schema to a
732
+ * module the mock validates its calls against. A build-time view, as
733
+ * apiSignatureEntries() is: it comes off the server instance, which a
734
+ * generator imports and a client never does.
735
+ */
736
+ async apiSchemaEntries() {
737
+ await this.loadApiGroups();
738
+ const entries = {};
739
+ for (const name of [...this.apiDefinitions.keys()].sort()) {
740
+ // Both are always there on the server: registration takes them from the declaration, which requires them.
741
+ const { input, output } = this.apiDefinitions.get(name);
742
+ entries[name] = { input: input, output: output };
743
+ }
744
+ return entries;
745
+ }
726
746
  getResponseBuilder(ctx) {
727
747
  return new LambderResponseBuilder({
728
748
  files: this.files,
@@ -759,8 +779,8 @@ export default class Lambder {
759
779
  ensureInitialized() {
760
780
  if (!this.initPromise) {
761
781
  const pending = (async () => {
762
- for (const hookFn of this.createdHooks) {
763
- await hookFn(this);
782
+ for (const hook of this.hookList["created"]) {
783
+ await hook.hookFn(this);
764
784
  }
765
785
  })();
766
786
  this.initPromise = pending;
@@ -828,11 +848,12 @@ export default class Lambder {
828
848
  for (const hook of this.hookList["fallback"]) {
829
849
  await hook.hookFn(currentCtx, resolver);
830
850
  }
831
- // A request under apiPath is the API's to answer, whatever it asked
832
- // for. A root apiPath shares every path with the site, so there only
833
- // apiPath itself and the calls are.
851
+ // A request at or under apiPath is the API's to answer, whatever it
852
+ // asked for. A root apiPath shares every path with the site, the site
853
+ // root included, so there only the calls are.
834
854
  const apiArea = this.apiPath.replace(/\/+$/, "");
835
- const isAPI = currentCtx.api !== null || currentCtx.path === this.apiPath || (apiArea !== "" && currentCtx.path.startsWith(`${apiArea}/`));
855
+ const isAPI = currentCtx.api !== null
856
+ || (apiArea !== "" && (currentCtx.path === this.apiPath || currentCtx.path.startsWith(`${apiArea}/`)));
836
857
  if (isAPI) {
837
858
  if (this.apiFallbackHandler)
838
859
  return await this.apiFallbackHandler(currentCtx, resolver);
@@ -871,8 +892,9 @@ export default class Lambder {
871
892
  return responseFromAnswer(versionExpiredAnswer(this.apiVersion));
872
893
  // The protocol's own pre-pass, run here rather than left to the
873
894
  // pipeline so that hooks and route matching see a plain payload,
874
- // and so a stale client is answered before any of them, whether or
875
- // not the name it asked for exists.
895
+ // and so a stale client, or a body that is no envelope, is
896
+ // answered before any of them, whether or not the name it asked
897
+ // for exists.
876
898
  const prepared = await this.pipeline.prepare(ctx.api);
877
899
  if (prepared)
878
900
  return responseFromAnswer(prepared);
@@ -1141,14 +1163,14 @@ export default class Lambder {
1141
1163
  * The answer to a request that needed a session and has none, whether it
1142
1164
  * never had one or it ended while the request held it: an API call gets
1143
1165
  * the protocol's sessionExpired envelope, as the pipeline gives a session
1144
- * API, and anything else the setSessionExpiredRouteHandler answer, a 401
1145
- * by default.
1166
+ * API, and anything else the setSessionExpiredRouteHandler answer, a
1167
+ * plain-text 401 by default, as the framework's own 404 and 500 are.
1146
1168
  */
1147
1169
  async sessionMissingResponse(ctx, resolver) {
1148
1170
  if (ctx.api)
1149
1171
  return responseFromAnswer(sessionExpiredAnswer(this.apiVersion, ctx.logList));
1150
1172
  if (!this.sessionExpiredRouteHandler)
1151
- return resolver.status(401, "Session required.");
1173
+ return resolver.text("Session required.", { statusCode: 401 });
1152
1174
  try {
1153
1175
  return await this.sessionExpiredRouteHandler(ctx, resolver);
1154
1176
  }
@@ -1275,16 +1297,17 @@ export default class Lambder {
1275
1297
  }
1276
1298
  /**
1277
1299
  * A thrown LambderApiRefusal (from a hook, say) as the structured API
1278
- * envelope: the core's one mapping, after the same check the pipeline
1279
- * applies, against the endpoint the call names, its lazy group loaded if
1280
- * the refusal came before the call reached it. A name no API is
1281
- * registered under declares no code, so only an uncoded or a framework
1282
- * refusal goes out for it.
1300
+ * envelope: the pipeline's own answer to a refusal (answerRefusal), so
1301
+ * it is checked against the endpoint the call names, its lazy group
1302
+ * loaded if the refusal came before the call reached it, and one saying
1303
+ * the session is over ends it. A name no API is registered under
1304
+ * declares no code, so only an uncoded or a framework refusal goes out
1305
+ * for it.
1283
1306
  */
1284
1307
  async apiErrorResponse(err, ctx) {
1285
1308
  const apiName = ctx.apiName ?? "";
1286
- const refusal = checkedRefusal(apiName, (await this.registeredDefinitionOf(apiName))?.refusals ?? NO_DECLARED_REFUSALS, err);
1287
- return responseFromAnswer(refusalAnswer(refusal, this.apiVersion, ctx.logList));
1309
+ const refusals = (await this.registeredDefinitionOf(apiName))?.refusals ?? NO_DECLARED_REFUSALS;
1310
+ return responseFromAnswer(await this.pipeline.answerRefusal(apiName, refusals, err, ctx));
1288
1311
  }
1289
1312
  }
1290
1313
  /** The init bound to one vocabulary (or none): the policy builders, refuse and create() that share it. */
@@ -1305,11 +1328,45 @@ const lambderInitOf = (declared) => ({
1305
1328
  },
1306
1329
  });
1307
1330
  /**
1308
- * The entry point of an app: binds the session data type, and hands out the
1309
- * builders and create() that share it. `declareRefusals()` binds the app's
1331
+ * The entry point of an app, and the canonical way to create an instance:
1332
+ * fix the session data type first, then create with the full configuration
1333
+ * in one declaration. The policy, guard and idempotency types are inferred
1334
+ * from the options, so the instance is born fully typed, and the endpoints
1335
+ * declared with its defineApi are typed against it. There are no ordering
1336
+ * rules, and nothing can be half-configured.
1337
+ *
1338
+ * Beside create() it hands out the builders that share the session data
1339
+ * type (guard, rateLimitKey, refuse). `declareRefusals()` binds the app's
1310
1340
  * refusal vocabulary too, so a guard's ctx.refuse and the init's own refuse
1311
1341
  * are typed to it before any instance exists, and create() gives it to the
1312
1342
  * instance for every API's refusals option to name codes from.
1343
+ *
1344
+ * ```typescript
1345
+ * // app.ts (imports no api modules, so modules can import from it)
1346
+ * export const lambderApp = initLambder<SessionData>().create({
1347
+ * apiPath: "/api",
1348
+ * session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: "..." },
1349
+ * rateLimits: { limiter, policies },
1350
+ * guards,
1351
+ * idempotency: { store },
1352
+ * });
1353
+ * export const { defineApi, defineApiGroup, lazyApiGroup } = lambderApp;
1354
+ *
1355
+ * // orders.ts
1356
+ * export const orderApis = defineApiGroup("orders", {
1357
+ * place: defineApi({ input, output, guards: "signedIn" }, async (ctx) => ...),
1358
+ * });
1359
+ *
1360
+ * // index.ts: registration only
1361
+ * const lambder = lambderApp.registerApiGroups(orderApis).addHook(...);
1362
+ * export const handler = lambder.getHandler();
1363
+ * ```
1364
+ *
1365
+ * Curried because TypeScript type arguments are all-or-nothing per call:
1366
+ * passing the session data type to `new Lambder<S>(...)` would silently
1367
+ * widen the inferred policy and guard types to their {} defaults. Fixing the
1368
+ * session type in the first call lets the second infer everything else.
1369
+ * `new Lambder(options)` serves untyped or session-data-free instances.
1313
1370
  */
1314
1371
  export const initLambder = () => ({
1315
1372
  ...lambderInitOf(null),
@@ -27,7 +27,7 @@ export type LambderCallSummary = {
27
27
  coldStart: boolean;
28
28
  /** The invocation's request id, the one Lambda's own log lines for it carry. */
29
29
  requestId: string | null;
30
- /** The request id of the invocation that called this one over a direct invoke (LambderInvokeCaller), which carries it; null otherwise. */
30
+ /** The request id of the invocation that called this one over a Lambda invoke (LambderInvokeCaller), which carries it; null otherwise. */
31
31
  parentRequestId: string | null;
32
32
  };
33
33
  /**