lambder 14.1.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 (158) hide show
  1. package/CHANGELOG.md +407 -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 +25 -11
  12. package/dist/api/LambderApiPipeline.js +34 -17
  13. package/dist/api/LambderApiRateLimits.d.ts +28 -21
  14. package/dist/api/LambderApiRateLimits.js +55 -35
  15. package/dist/api/LambderApiRefusals.d.ts +10 -4
  16. package/dist/api/LambderApiRefusals.js +31 -5
  17. package/dist/api/LambderApiRequest.d.ts +21 -1
  18. package/dist/api/LambderApiRequest.js +31 -2
  19. package/dist/build/checkApiRefusals.d.ts +7 -2
  20. package/dist/build/checkApiRefusals.js +56 -29
  21. package/dist/build/contractPrinterProcess.d.ts +11 -0
  22. package/dist/build/contractPrinterProcess.js +17 -0
  23. package/dist/build/generateApiFiles.d.ts +26 -5
  24. package/dist/build/generateApiFiles.js +68 -7
  25. package/dist/build/writeApiSchemas.d.ts +109 -0
  26. package/dist/build/writeApiSchemas.js +308 -0
  27. package/dist/build.d.ts +3 -1
  28. package/dist/build.js +2 -0
  29. package/dist/client/LambderCaller.d.ts +25 -11
  30. package/dist/client/LambderCaller.js +57 -49
  31. package/dist/client/LambderUploadRunner.d.ts +55 -27
  32. package/dist/client/LambderUploadRunner.js +89 -34
  33. package/dist/client.d.ts +6 -6
  34. package/dist/client.js +2 -2
  35. package/dist/core/Lambder.d.ts +31 -21
  36. package/dist/core/Lambder.js +79 -23
  37. package/dist/core/LambderCallSummary.d.ts +1 -1
  38. package/dist/core/LambderContext.d.ts +59 -7
  39. package/dist/core/LambderContext.js +31 -15
  40. package/dist/core/LambderCrashHandling.d.ts +5 -0
  41. package/dist/core/LambderCrashHandling.js +11 -1
  42. package/dist/core/LambderCreateOptions.d.ts +28 -17
  43. package/dist/core/LambderCreateOptions.js +14 -7
  44. package/dist/core/LambderResolver.d.ts +3 -2
  45. package/dist/core/LambderResponseBuilder.d.ts +20 -9
  46. package/dist/core/LambderResponseBuilder.js +39 -8
  47. package/dist/core/LambderTemplatingEngine.d.ts +24 -6
  48. package/dist/core/LambderTemplatingEngine.js +23 -3
  49. package/dist/index.d.ts +15 -12
  50. package/dist/index.js +7 -5
  51. package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
  52. package/dist/invoke/LambderInvokeCaller.js +47 -9
  53. package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
  54. package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
  55. package/dist/mock/LambderMockAdapterDoor.js +10 -0
  56. package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
  57. package/dist/mock/LambderMockApiSchemas.js +150 -0
  58. package/dist/mock/LambderMockApp.d.ts +56 -48
  59. package/dist/mock/LambderMockApp.js +107 -62
  60. package/dist/mock/LambderMockBrowserCookies.js +18 -5
  61. package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
  62. package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
  63. package/dist/mock/LambderMockFailureInjector.js +12 -8
  64. package/dist/mock/LambderMockTypes.d.ts +8 -4
  65. package/dist/mock/lambderMockInvokeTransport.js +2 -9
  66. package/dist/mock/lambderMockMswHandler.d.ts +19 -13
  67. package/dist/mock/lambderMockMswHandler.js +25 -22
  68. package/dist/mock.d.ts +1 -0
  69. package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
  70. package/dist/secrets/LambderOneShotSecrets.js +6 -36
  71. package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
  72. package/dist/secrets/LambderPasswordHasher.js +168 -0
  73. package/dist/session/LambderSessionController.d.ts +37 -4
  74. package/dist/session/LambderSessionController.js +36 -6
  75. package/dist/session/LambderSessionManager.d.ts +33 -3
  76. package/dist/session/LambderSessionManager.js +55 -16
  77. package/dist/shared/LambderHtml.d.ts +23 -4
  78. package/dist/shared/LambderHtml.js +17 -5
  79. package/dist/shared/LambderI18n.d.ts +106 -16
  80. package/dist/shared/LambderI18n.js +259 -74
  81. package/dist/shared/contracts/LambderCache.d.ts +14 -1
  82. package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
  83. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  84. package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
  85. package/dist/shared/util/LambderBackoffTimer.js +40 -12
  86. package/dist/shared/util/LambderCallAbort.d.ts +22 -6
  87. package/dist/shared/util/LambderCallAbort.js +37 -9
  88. package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
  89. package/dist/shared/util/LambderErrorReportMark.js +22 -0
  90. package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
  91. package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
  92. package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
  93. package/dist/shared/util/LambderSignedClaims.js +54 -0
  94. package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
  95. package/dist/shared/util/LambderSwappableInstances.js +54 -0
  96. package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
  97. package/dist/shared/util/LambderTestingDoors.js +6 -2
  98. package/dist/shared/util/LambderTextDigest.d.ts +5 -5
  99. package/dist/shared/util/LambderTextDigest.js +5 -5
  100. package/dist/shared/wire/LambderApiContract.d.ts +1 -1
  101. package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
  102. package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
  103. package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
  104. package/dist/shared/wire/LambderApiOutcome.d.ts +21 -12
  105. package/dist/shared/wire/LambderApiOutcome.js +1 -1
  106. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
  107. package/dist/shared/wire/LambderApiRefusal.js +3 -3
  108. package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
  109. package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
  110. package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
  111. package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
  112. package/dist/shared/wire/LambderCallOutcome.js +3 -2
  113. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
  114. package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
  115. package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
  116. package/dist/shared/wire/LambderInvokeApiId.js +3 -3
  117. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
  118. package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
  119. package/dist/stores/LambderCacheValues.d.ts +2 -5
  120. package/dist/stores/LambderDdbCache.d.ts +15 -9
  121. package/dist/stores/LambderDdbCache.js +25 -6
  122. package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
  123. package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
  124. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
  125. package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
  126. package/dist/stores/LambderDdbSdk.d.ts +59 -29
  127. package/dist/stores/LambderDdbSdk.js +116 -44
  128. package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
  129. package/dist/stores/LambderDdbSessionStore.js +133 -68
  130. package/dist/stores/LambderMemoryCache.d.ts +2 -2
  131. package/dist/stores/LambderS3FileSource.d.ts +3 -2
  132. package/dist/stores/LambderS3FileSource.js +11 -13
  133. package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
  134. package/dist/stores/LambderS3UploadBucket.js +23 -0
  135. package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
  136. package/dist/stores/LambderStoredText.d.ts +26 -0
  137. package/dist/stores/LambderStoredText.js +25 -0
  138. package/dist/testing/LambderConformanceRunner.d.ts +9 -2
  139. package/dist/testing/LambderConformanceRunner.js +9 -2
  140. package/dist/testing/LambderTestApp.d.ts +65 -7
  141. package/dist/testing/LambderTestApp.js +101 -6
  142. package/dist/testing/LambderTestVisitor.d.ts +43 -12
  143. package/dist/testing/LambderTestVisitor.js +35 -11
  144. package/dist/testing/bootLambdaPackage.d.ts +105 -0
  145. package/dist/testing/bootLambdaPackage.js +265 -0
  146. package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
  147. package/dist/testing/lambdaPackageBootProcess.js +130 -0
  148. package/dist/testing/lambderCacheConformance.d.ts +26 -0
  149. package/dist/testing/lambderCacheConformance.js +287 -0
  150. package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
  151. package/dist/testing/lambderCacheStorageConformance.js +159 -0
  152. package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
  153. package/dist/testing/lambderSessionStoreConformance.js +3 -3
  154. package/dist/testing.d.ts +7 -1
  155. package/dist/testing.js +5 -0
  156. package/package.json +1 -6
  157. package/dist/shared/util/boundKeyField.d.ts +0 -20
  158. package/dist/shared/util/boundKeyField.js +0 -34
@@ -5,6 +5,7 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
5
5
  import { type LambderResponseTools } from "../api/LambderApiCallContext.js";
6
6
  import type LambderSessionController from "../session/LambderSessionController.js";
7
7
  import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult } from "../api/LambderApiRateLimits.js";
8
+ import type { LambderAppTypes } from "../api/LambderApiDeclarations.js";
8
9
  export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
9
10
  /**
10
11
  * Which API Gateway payload format an event arrived in, and the format its
@@ -15,6 +16,21 @@ export type LambderHttpEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
15
16
  export type LambderHttpEventFormat = "v1" | "v2";
16
17
  /** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
17
18
  export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxyEventV2;
19
+ /**
20
+ * How a request reached the function, as `ctx.arrivedVia` says it:
21
+ * - "proxy": over HTTP, carrying a valid `originProof`, so it came through
22
+ * the proxy in front of the app;
23
+ * - "direct": over HTTP without a valid proof, sent to the gateway's own
24
+ * address (an `execute-api` or `lambda-url` domain) rather than through the
25
+ * proxy, so the proxy's headers on it are whatever its sender wrote and
26
+ * have been taken off;
27
+ * - "invoke": a Lambda invoke, told by the event's `requestContext.apiId`,
28
+ * which a gateway writes itself and no HTTP client can set, so only a
29
+ * caller IAM let invoke the function sends one;
30
+ * - "unverified": over HTTP on an instance with no `originProof`, where
31
+ * nothing tells a proxied request from a direct one.
32
+ */
33
+ export type LambderRequestArrival = "proxy" | "direct" | "invoke" | "unverified";
18
34
  /**
19
35
  * Everything a route or API handler knows about the request. Extends the
20
36
  * API core's call context (session, guardData, responseHeaders, logList),
@@ -24,15 +40,17 @@ export declare const isV2HttpEvent: (event: unknown) => event is APIGatewayProxy
24
40
  * and the response tools (setResponseHeader, addResponseHeader, setCookie,
25
41
  * clearCookie) that write onto whatever answer the request ends with.
26
42
  *
27
- * TRateLimitPolicies is the app's policies map on a handler registered with
28
- * defineApi, addRoute or addSessionRoute, so a policy name is
29
- * checked where it is charged; anywhere else (a hook, a guard) the names are
30
- * any string.
43
+ * TRateLimitPolicies is the app's policies map on an API handler (defineApi)
44
+ * and on a route registered by a path string (addRoute, addSessionRoute), so
45
+ * a policy name is checked where it is charged; anywhere else (a route
46
+ * matched by a RegExp, a predicate or a matcher object, a hook, a guard) the
47
+ * names are any string. LambderRenderContextOf names the typed form for a
48
+ * helper that takes one instance's context.
31
49
  */
32
50
  export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TSessionData = any, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = {
33
51
  /**
34
52
  * The Host the gateway received, or the first header named in
35
- * `trustedHostHeaders` that carries a well-formed host. On a direct
53
+ * `trustedHostHeaders` that carries a well-formed host. On a Lambda
36
54
  * invoke, the invoking caller's `host`, whatever headers it forwarded.
37
55
  */
38
56
  host: string;
@@ -93,7 +111,7 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
93
111
  /**
94
112
  * The address the gateway observed, or the leftmost entry of the first
95
113
  * header named in `trustedClientIpHeaders` that carries one; nothing is
96
- * trusted by default, and no header on a direct invoke, whose address is
114
+ * trusted by default, and no header on a Lambda invoke, whose address is
97
115
  * the invoking caller's `clientIp`. One spelling per address (port and brackets
98
116
  * stripped, lowercased, length-bounded), so a `per: "ip"` limit keys one
99
117
  * counter per client.
@@ -105,6 +123,12 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
105
123
  lambdaContext: Context;
106
124
  /** Which API Gateway payload format the event arrived in, and the response leaves in. */
107
125
  eventFormat: LambderHttpEventFormat;
126
+ /**
127
+ * How the request reached the function (see LambderRequestArrival):
128
+ * through the proxy with a valid `originProof`, without it, by a
129
+ * Lambda invoke, or, with no `originProof` configured, unverified.
130
+ */
131
+ readonly arrivedVia: LambderRequestArrival;
108
132
  /** Response headers written during the request (the response tools below, session cookies), applied onto the response at the end. */
109
133
  responseHeaders: LambderAnswerHeaders;
110
134
  /** Entries for the API envelope's logList channel: a handler pushes what it wants the caller's debug log to show. */
@@ -132,6 +156,25 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
132
156
  export type LambderSessionRenderContext<TApiPayload = any, SessionData = any, TPathParams extends Record<string, string> = Record<string, string>, TGuardData = {}, TRateLimitPolicies = Record<string, LambderApiRateLimitPolicyConfig>> = Omit<LambderRenderContext<TApiPayload, TPathParams, TGuardData, SessionData, TRateLimitPolicies>, 'session'> & {
133
157
  session: LambderSessionRecord<SessionData>;
134
158
  };
159
+ /** What the context helpers below read off an instance: the AppTypes property every instance carries. */
160
+ type LambderInstanceTypes = {
161
+ readonly AppTypes: LambderAppTypes;
162
+ };
163
+ /**
164
+ * One instance's render context, read off `typeof lambderApp`: its session
165
+ * data and its rate-limit policy names, any payload, path parameters and
166
+ * guard data. What a helper in another file takes to be handed a context of
167
+ * that instance, without writing LambderRenderContext's parameters out: an
168
+ * API handler's, a route's, a hook's and a guard's context are each
169
+ * assignable to it.
170
+ */
171
+ export type LambderRenderContextOf<TInstance extends LambderInstanceTypes> = LambderRenderContext<any, Record<string, string>, {}, TInstance["AppTypes"]["session"], TInstance["AppTypes"]["policies"]>;
172
+ /**
173
+ * LambderRenderContextOf with the session present: what a helper takes to be
174
+ * handed the context of a session route, a session endpoint's handler or a
175
+ * guard that needs a session, each assignable to it.
176
+ */
177
+ export type LambderSessionRenderContextOf<TInstance extends LambderInstanceTypes> = LambderSessionRenderContext<any, TInstance["AppTypes"]["session"], Record<string, string>, {}, TInstance["AppTypes"]["policies"]>;
135
178
  /** The members of a render context that are bound onto it rather than read from its event. */
136
179
  type LambderContextToolName = "sessionController" | "rateLimit" | "isRateLimited" | keyof LambderResponseTools;
137
180
  /** What an instance binds onto each context it renders: see bindContextTools. */
@@ -157,6 +200,15 @@ export type LambderOriginProof = {
157
200
  header: string;
158
201
  /** The values it may carry: the current secret, and during a rotation the one before it. */
159
202
  secrets: readonly string[];
203
+ /**
204
+ * Headers the proxy writes that the app reads, beside the trusted client
205
+ * address and host headers, such as "cf-ipcountry" or
206
+ * "cloudfront-viewer-country". A request without the proof carries
207
+ * whatever its sender wrote under these names, so they are taken off
208
+ * `ctx.headers` and `ctx.header()` there, as the trusted headers are.
209
+ * Default: none.
210
+ */
211
+ proxyHeaders?: readonly string[];
160
212
  };
161
213
  /** What createContext reads a request with: the instance's own settings for where an API call goes and which forwarded headers it trusts. */
162
214
  export type LambderContextOptions = {
@@ -166,7 +218,7 @@ export type LambderContextOptions = {
166
218
  trustedClientIpHeaders?: readonly string[];
167
219
  /** See `trustedHostHeaders` at create(). Default: none. */
168
220
  trustedHostHeaders?: readonly string[];
169
- /** See `originProof` at create(). Default: none, so the trusted headers are read on every request. */
221
+ /** See `originProof` at create(). Default: none, so the trusted headers are read on every request and `arrivedVia` is "unverified" on all but an invoke. */
170
222
  originProof?: LambderOriginProof | null;
171
223
  };
172
224
  /** The render context for one request: everything a route handler, an API handler, a hook or a guard reads about it, built once from the Lambda event. */
@@ -131,25 +131,31 @@ export const createContext = (event, lambdaContext, { apiPath = DEFAULT_API_PATH
131
131
  // The origin proof is read, then taken off the headers the app reads
132
132
  // (ctx.headers, ctx.header()), so no handler, hook or log of them meets
133
133
  // the secret; the raw event keeps it. A request that does not carry it
134
- // came to the origin some other way than through the proxy, and the
135
- // proxy's headers on it are whatever its sender wrote.
134
+ // came to the origin some other way than through the proxy, and every
135
+ // header the proxy writes is, on it, whatever its sender wrote: those
136
+ // are taken off too, so a handler reading one (a country, a viewer's
137
+ // address) reads what the proxy said or nothing.
136
138
  let proven = true;
137
139
  if (originProof) {
138
- const proofHeader = originProof.header.toLowerCase();
139
- const presented = lowercasedHeaders[proofHeader] ?? "";
140
+ const presented = lowercasedHeaders[originProof.header.toLowerCase()] ?? "";
140
141
  proven = originProof.secrets.some((secret) => constantTimeEquals(presented, secret));
141
- delete lowercasedHeaders[proofHeader];
142
- headers = Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== proofHeader));
142
+ const senderWritten = proven ? [] : [...trustedClientIpHeaders, ...trustedHostHeaders, ...(originProof.proxyHeaders ?? [])];
143
+ const removed = new Set([originProof.header, ...senderWritten].map((name) => name.toLowerCase()));
144
+ for (const name of removed)
145
+ delete lowercasedHeaders[name];
146
+ headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !removed.has(name.toLowerCase())));
143
147
  }
144
148
  const header = (name) => lowercasedHeaders[name.toLowerCase()];
145
149
  // A trusted forwarding header is trusted because a proxy in front of this
146
- // function writes it. A direct invoke has no such proxy: its headers are
150
+ // function writes it. A Lambda invoke has no such proxy: its headers are
147
151
  // whatever the invoking code passed on, and a gateway lambda forwarding a
148
152
  // browser's request passes on the browser's own. So on an invoke the
149
153
  // address and the host are the ones the invoker named (clientIp and host,
150
- // delivered as sourceIp and Host), and no header is read for either.
154
+ // delivered as sourceIp and Host), and no header is read for either. An
155
+ // invoke is told by the apiId, which a gateway writes itself.
151
156
  const invokedDirectly = event.requestContext?.apiId === LAMBDER_INVOKE_API_ID;
152
157
  const readsForwarding = proven && !invokedDirectly;
158
+ const arrivedVia = invokedDirectly ? "invoke" : !originProof ? "unverified" : proven ? "proxy" : "direct";
153
159
  // The first trusted header carrying a well-formed host, leftmost entry,
154
160
  // else the host the gateway saw. Behind CloudFront a Function URL sees
155
161
  // its own lambda-url domain, since CloudFront sends an origin its own
@@ -163,21 +169,30 @@ export const createContext = (event, lambdaContext, { apiPath = DEFAULT_API_PATH
163
169
  }
164
170
  }
165
171
  const ip = resolveClientIp(lowercasedHeaders, sourceIp, readsForwarding ? trustedClientIpHeaders : []);
166
- // Decode body: keep the raw string, then parse as JSON with urlencoded fallback.
172
+ // Decode body: keep the raw string, then read it as a JSON object, or as
173
+ // urlencoded fields when it is not JSON. ctx.post is an object whatever
174
+ // the body was: a JSON number, string, boolean, null or array is not
175
+ // fields, and every reader (a handler, a hook, the API path writing the
176
+ // restored payload onto it) indexes into it. Such a body stays readable
177
+ // as ctx.rawBody. What it means for an API call is readApiEnvelope's to
178
+ // say, from the parse itself.
167
179
  const rawBody = event.isBase64Encoded
168
180
  ? (event.body ? base64ToText(event.body) : "")
169
181
  : (event.body || "");
182
+ let posted;
183
+ let bodyNotJson = false;
170
184
  let post = {};
171
185
  try {
172
- post = JSON.parse(rawBody || "{}") || {};
186
+ posted = JSON.parse(rawBody || "{}");
173
187
  }
174
- catch (e) {
175
- const params = new URLSearchParams(rawBody);
176
- post = {};
177
- for (const [key, value] of params.entries()) {
188
+ catch {
189
+ bodyNotJson = true;
190
+ for (const [key, value] of new URLSearchParams(rawBody).entries()) {
178
191
  post[key] = value;
179
192
  }
180
193
  }
194
+ if (posted !== null && typeof posted === "object" && !Array.isArray(posted))
195
+ post = posted;
181
196
  // A JSON POST to `{apiPath}/{group}/{action}` is a call to that endpoint;
182
197
  // the core reads the envelope, and everything downstream reads ctx.api.
183
198
  // JSON only (isApiCallContentType): any site can submit a plain HTML form
@@ -188,7 +203,7 @@ export const createContext = (event, lambdaContext, { apiPath = DEFAULT_API_PATH
188
203
  const isJsonPost = method === "POST" && !!apiPath && isApiCallContentType(lowercasedHeaders);
189
204
  const calledName = isJsonPost ? apiNameOfCallPath(apiPath, path) : null;
190
205
  const requestInfo = { headers: lowercasedHeaders, cookies: cookieList, ip, host };
191
- const api = calledName !== null ? readApiEnvelope(post, requestInfo, calledName)
206
+ const api = calledName !== null ? readApiEnvelope(posted, requestInfo, calledName, bodyNotJson ? { bodyNotJson: true } : {})
192
207
  : isJsonPost && path === apiPath && typeof post.apiName === "string" && post.apiName !== ""
193
208
  ? readApiEnvelope(post, requestInfo, post.apiName, { retiredPath: true })
194
209
  : null;
@@ -203,6 +218,7 @@ export const createContext = (event, lambdaContext, { apiPath = DEFAULT_API_PATH
203
218
  headers, rawBody, ip, header,
204
219
  lambdaContext,
205
220
  eventFormat,
221
+ arrivedVia,
206
222
  responseHeaders: new LambderAnswerHeaders(),
207
223
  logList: [],
208
224
  }, UNBOUND_CONTEXT_TOOLS);
@@ -22,6 +22,11 @@ export declare class LambderCrashHandling {
22
22
  * with it as unfinished, and either way the request goes on to be
23
23
  * answered. The report itself cannot be cancelled and runs on; only the
24
24
  * wait ends.
25
+ *
26
+ * An error already reported where it arose (an invoke failure its
27
+ * caller's onFailure took, which api() then threw) is not handed over
28
+ * again: it is still a crash, answered as one, but reporting it here
29
+ * would record the one failure twice.
25
30
  */
26
31
  report(error: Error, site: LambderCrashSite): Promise<void>;
27
32
  /**
@@ -1,6 +1,7 @@
1
1
  import { crashAnswer } from "../api/LambderApiEnvelope.js";
2
2
  import { describeCrash } from "../shared/wire/LambderCrashDetail.js";
3
3
  import { stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
4
+ import { isErrorReported } from "../shared/util/LambderErrorReportMark.js";
4
5
  import { LambderResponse, responseFromAnswer } from "./LambderResponse.js";
5
6
  /**
6
7
  * How long a crash's answer waits for the reporter by default: long enough
@@ -33,10 +34,15 @@ export class LambderCrashHandling {
33
34
  * with it as unfinished, and either way the request goes on to be
34
35
  * answered. The report itself cannot be cancelled and runs on; only the
35
36
  * wait ends.
37
+ *
38
+ * An error already reported where it arose (an invoke failure its
39
+ * caller's onFailure took, which api() then threw) is not handed over
40
+ * again: it is still a crash, answered as one, but reporting it here
41
+ * would record the one failure twice.
36
42
  */
37
43
  async report(error, site) {
38
44
  const report = this.options.report;
39
- if (!report)
45
+ if (!report || isErrorReported(error))
40
46
  return;
41
47
  const deadline = AbortSignal.timeout(this.reportTimeoutMs);
42
48
  try {
@@ -74,8 +80,12 @@ export class LambderCrashHandling {
74
80
  if (ctx?.api) {
75
81
  return responseFromAnswer(crashAnswer(this.apiVersion, revealed ? { crash: revealed, logList: ctx.logList } : undefined));
76
82
  }
83
+ // Typed as the text it is, as the framework's own 404 is: with no
84
+ // Content-Type a browser sniffs the body, and a crash message that
85
+ // reads like markup would render as a page.
77
86
  return new LambderResponse({
78
87
  statusCode: 500,
88
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
79
89
  body: revealed
80
90
  ? ["Internal Server Error.", "", revealed.stack ?? `${revealed.name}: ${revealed.message}`,
81
91
  ...(revealed.causeList ?? []).map((cause) => `Caused by: ${cause.stack ?? `${cause.name}: ${cause.message}`}`)].join("\n")
@@ -128,7 +128,14 @@ export type LambderSessionOptions<TSessionData = any> = {
128
128
  * the store, so ctx.session.data would be typed by the table.
129
129
  */
130
130
  store: LambderSessionStore<any>;
131
- /** The HMAC key that turns a sessionKey into the store's partition key, so a table read does not reveal whose sessions it holds. Treat as a secret. */
131
+ /**
132
+ * The HMAC key that turns a sessionKey into the store's partition key, so
133
+ * a table read does not reveal whose sessions it holds. A subkey derived
134
+ * from it (HKDF) also keys the at-rest digests of the caller's fields in
135
+ * every rate-limit key and idempotency scope. Changing it signs every
136
+ * session out and starts every rate-limit counter and idempotency record
137
+ * afresh. Treat as a secret.
138
+ */
132
139
  sessionSalt: string;
133
140
  enableSlidingExpiration?: boolean;
134
141
  /** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
@@ -203,8 +210,8 @@ export type LambderCreateOptions<TSessionData = any> = {
203
210
  * a change the signatures cannot see (a security fix, a field whose
204
211
  * meaning changed under the same shape). Dotted numbers ("1.2.10"),
205
212
  * compared segment by segment; a call naming no version is not judged. A
206
- * floor above `apiVersion` is taken as `apiVersion`, with a warning, so a
207
- * mistaken floor cannot refuse this build's own clients. Default: none.
213
+ * floor above `apiVersion` throws at creation, since it would refuse this
214
+ * build's own clients. Default: none.
208
215
  */
209
216
  minApiVersion?: string;
210
217
  /**
@@ -248,7 +255,7 @@ export type LambderCreateOptions<TSessionData = any> = {
248
255
  * `per: "ip"` rate limits key off ctx.ip, and an address the caller picks
249
256
  * per request is not a limit. API Gateway APPENDS to x-forwarded-for, so
250
257
  * behind API Gateway alone the leftmost entry is the client's own claim
251
- * and the header should be left out. A direct invoke reads none of these:
258
+ * and the header should be left out. A Lambda invoke reads none of these:
252
259
  * its ctx.ip is the invoker's `clientIp`.
253
260
  */
254
261
  trustedClientIpHeaders?: readonly string[];
@@ -260,24 +267,28 @@ export type LambderCreateOptions<TSessionData = any> = {
260
267
  *
261
268
  * The same rule as trustedClientIpHeaders: ctx.host decides cookie
262
269
  * domains and host-matched routes, and a host a client picks is a tenant
263
- * a client picks. A direct invoke reads none of these: its ctx.host is the
270
+ * a client picks. A Lambda invoke reads none of these: its ctx.host is the
264
271
  * invoker's `host`.
265
272
  */
266
273
  trustedHostHeaders?: readonly string[];
267
274
  /**
268
275
  * Proof that a request came through the proxy the trusted headers are
269
- * trusted for: `{ header, secrets }`, a header the proxy sets on every
270
- * request it forwards (a Cloudflare transform rule, a CloudFront origin
271
- * custom header) and the secret it carries, with the previous one beside
272
- * it during a rotation. The trusted client address and host headers are
273
- * read only from a request that carries it. One sent to the origin
274
- * directly (an API Gateway's own execute-api URL, a Function URL) has
275
- * its address and host from the gateway, whatever headers it wrote, so
276
- * a `per: "ip"` limit counts it by the address it really came from. The
277
- * header is taken off `ctx.headers` and `ctx.header()`, so no handler or
278
- * hook meets the secret. Default: none, so the trusted headers are read
279
- * on every request, which is right only for an origin nothing but the
280
- * proxy can reach.
276
+ * trusted for: `{ header, secrets, proxyHeaders? }`, a header the proxy
277
+ * sets on every request it forwards (a Cloudflare transform rule, a
278
+ * CloudFront origin custom header) and the secret it carries, with the
279
+ * previous one beside it during a rotation. The trusted client address
280
+ * and host headers are read only from a request that carries it. One
281
+ * sent to the origin directly (an API Gateway's own execute-api URL, a
282
+ * Function URL) has its address and host from the gateway, whatever
283
+ * headers it wrote, so a `per: "ip"` limit counts it by the address it
284
+ * really came from, and the trusted headers and `proxyHeaders` (the
285
+ * other headers only the proxy writes) are taken off `ctx.headers` and
286
+ * `ctx.header()` on it. `ctx.arrivedVia` says which it was: "proxy" or
287
+ * "direct", or "invoke" for a Lambda invoke. The proof header itself is
288
+ * always taken off, so no handler or hook meets the secret. Default:
289
+ * none, so the trusted headers are read on every request, which is right
290
+ * only for an origin nothing but the proxy can reach, and
291
+ * `ctx.arrivedVia` is "unverified" on all but an invoke.
281
292
  */
282
293
  originProof?: LambderOriginProof;
283
294
  /**
@@ -54,10 +54,16 @@ export const assertCreateOptions = (options) => {
54
54
  };
55
55
  /** Shortest secret an origin proof takes: one a sender could guess is no proof. 32 characters of hex or base64 is 128 bits or more. */
56
56
  export const MIN_ORIGIN_PROOF_SECRET_LENGTH = 32;
57
- /** An origin proof that proves something: a header name, secrets long enough not to guess, and trusted headers for it to guard. */
57
+ /** A header name as a proxy writes one. */
58
+ const HEADER_NAME_PATTERN = /^[A-Za-z0-9-]+$/;
59
+ /**
60
+ * An origin proof that proves something: a header name, secrets long enough
61
+ * not to guess, and proxy headers that are header names. It needs no
62
+ * trusted header to guard: ctx.arrivedVia is worth having on its own.
63
+ */
58
64
  const assertOriginProof = (options) => {
59
65
  const proof = options.originProof;
60
- if (typeof proof?.header !== "string" || !/^[A-Za-z0-9-]+$/.test(proof.header)) {
66
+ if (typeof proof?.header !== "string" || !HEADER_NAME_PATTERN.test(proof.header)) {
61
67
  throw new Error("Lambder: originProof.header must be a header name, such as \"x-origin-proof\".");
62
68
  }
63
69
  if (!Array.isArray(proof.secrets) || proof.secrets.length === 0) {
@@ -68,11 +74,12 @@ const assertOriginProof = (options) => {
68
74
  throw new Error(`Lambder: every originProof secret must be a string of at least ${MIN_ORIGIN_PROOF_SECRET_LENGTH} characters; a shorter one could be guessed, and a guessed proof proves nothing.`);
69
75
  }
70
76
  }
71
- const trusted = [...(options.trustedClientIpHeaders ?? []), ...(options.trustedHostHeaders ?? [])];
72
- if (trusted.length === 0) {
73
- throw new Error("Lambder: originProof guards the trustedClientIpHeaders and trustedHostHeaders, and this instance trusts none, so it would prove nothing.");
77
+ const proxyHeaders = proof.proxyHeaders ?? [];
78
+ if (!Array.isArray(proxyHeaders) || proxyHeaders.some((name) => typeof name !== "string" || !HEADER_NAME_PATTERN.test(name))) {
79
+ throw new Error("Lambder: originProof.proxyHeaders must list header names the proxy writes, such as [\"cf-ipcountry\"].");
74
80
  }
75
- if (trusted.some((name) => name.toLowerCase() === proof.header.toLowerCase())) {
76
- throw new Error(`Lambder: originProof.header "${proof.header}" is also a trusted header; the proof is its own header, which the app never reads.`);
81
+ const proxyWritten = [...(options.trustedClientIpHeaders ?? []), ...(options.trustedHostHeaders ?? []), ...proxyHeaders];
82
+ if (proxyWritten.some((name) => name.toLowerCase() === proof.header.toLowerCase())) {
83
+ throw new Error(`Lambder: originProof.header "${proof.header}" is also a trusted or proxy header; the proof is its own header, which the app never reads.`);
77
84
  }
78
85
  };
@@ -1,5 +1,6 @@
1
- import LambderResponseBuilder from "./LambderResponseBuilder.js";
1
+ import LambderResponseBuilder, { type LambderTemplateFileOptions } from "./LambderResponseBuilder.js";
2
2
  import type { LambderResponse } from "./LambderResponse.js";
3
+ import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
3
4
  type SyncDie<T extends (...args: any[]) => LambderResponse> = (...args: Parameters<T>) => never;
4
5
  type AsyncDie<T extends (...args: any[]) => Promise<LambderResponse>> = (...args: Parameters<T>) => Promise<never>;
5
6
  /** The `res.die.*` surface: every builder method, throwing what it built. Internal to the resolver, which is the only thing that has one. */
@@ -16,7 +17,7 @@ interface DieResolverMethods {
16
17
  fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
17
18
  apiRefusal: SyncDie<LambderResponseBuilder["apiRefusal"]>;
18
19
  file: AsyncDie<LambderResponseBuilder["file"]>;
19
- templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
20
+ templateFile: <TNames extends string = string>(filePath: string, data?: LambderTemplateData<TNames>, options?: LambderTemplateFileOptions) => Promise<never>;
20
21
  }
21
22
  /**
22
23
  * Response builder passed to route handlers and hooks.
@@ -16,6 +16,11 @@ export type LambderResponseOptions = {
16
16
  /** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
17
17
  etag?: boolean | "auto";
18
18
  };
19
+ /** res.templateFile's options: a response's, and how the file is compiled. */
20
+ export type LambderTemplateFileOptions = LambderResponseOptions & {
21
+ /** Expose the <title> content as slot "title" and the point before </head> as slot "head" (see LambderTemplatingEngineOptions). */
22
+ htmlVirtualSlots?: boolean;
23
+ };
19
24
  export type LambderRawResponseInit = {
20
25
  statusCode: LambderHttpStatusCode;
21
26
  headers?: LambderHeadersInput;
@@ -44,11 +49,16 @@ export default class LambderResponseBuilder {
44
49
  private requireFiles;
45
50
  raw(init: LambderRawResponseInit): LambderResponse;
46
51
  json(data: Record<string, any>, options?: LambderResponseOptions): LambderResponse;
52
+ /** A plain-text body, sent as text: the place for a message, whatever status it goes out with (`{ statusCode }`). */
47
53
  text(data: string, options?: LambderResponseOptions): LambderResponse;
48
- xml(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
49
- html(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
50
- status(statusCode: LambderHttpStatusCode, body?: string, options?: LambderResponseOptions): LambderResponse;
51
- status404(data: string, options?: LambderResponseOptions): LambderResponse;
54
+ /** An XML document (a sitemap, a feed, SVG) built with xml`...` or marked safe with raw(). */
55
+ xml(data: LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
56
+ /** An HTML page built with html`...` or marked safe with raw(). */
57
+ html(data: LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
58
+ /** An HTML response with any status code, its body built like res.html's; no body sends an empty one. */
59
+ status(statusCode: LambderHttpStatusCode, body?: LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
60
+ /** An HTML 404, its body built like res.html's. */
61
+ status404(data: LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
52
62
  /**
53
63
  * A redirect to `url`, which may be a path or a whole URL. A path stays on
54
64
  * this origin: a leading run of slashes and backslashes collapses to one
@@ -72,12 +82,13 @@ export default class LambderResponseBuilder {
72
82
  * LambderTemplatingEngine (comment-based slots/conditionals) and return
73
83
  * it as an HTML response. The compiled template is cached on the
74
84
  * instance across warm invocations; a missing file throws (it is a
75
- * server-side configuration error, not a client 404). Set
76
- * htmlVirtualSlots to expose "title"/"head" slots on marker-less files.
85
+ * server-side configuration error, not a client 404), and so does a data
86
+ * key the file has no slot or condition for, naming the file. TNames, when
87
+ * given, is the file's slot and condition names, so a misspelled key is a
88
+ * compile error. Set htmlVirtualSlots to expose "title"/"head" slots on
89
+ * marker-less files.
77
90
  */
78
- templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
79
- htmlVirtualSlots?: boolean;
80
- }): Promise<LambderResponse>;
91
+ templateFile<TNames extends string = string>(filePath: string, data?: LambderTemplateData<TNames>, options?: LambderTemplateFileOptions): Promise<LambderResponse>;
81
92
  /**
82
93
  * An API call answered from outside its handler: what a hook, a fallback,
83
94
  * the input validation handler or the global error handler answers with.
@@ -1,6 +1,21 @@
1
1
  import { outcomeOfEnvelope } from "../shared/wire/LambderCallOutcome.js";
2
2
  import { LambderResponse } from "./LambderResponse.js";
3
+ import { LambderSafeHtml } from "../shared/LambderHtml.js";
3
4
  import { plainRefusalEnvelope } from "../api/LambderApiEnvelope.js";
5
+ /**
6
+ * The body of a response a browser renders as markup: safe markup only. A
7
+ * plain string is refused rather than sent, since nothing can tell markup an
8
+ * author wrote from text a request supplied (`"No match at " + ctx.path`),
9
+ * and the latter, sent as a page, runs as script. The tag escapes what it
10
+ * interpolates, raw() is the one visible place where trusted markup passes as
11
+ * it is, and text goes out as text.
12
+ */
13
+ const markupBody = (method, tag, body) => {
14
+ if (body instanceof LambderSafeHtml)
15
+ return body.value;
16
+ throw new TypeError(`Lambder: ${method} takes safe ${tag === "html" ? "HTML" : "XML"}, and was given ${body === null ? "null" : typeof body}. Build the body with ${tag}\`...\`, which ` +
17
+ `escapes what it interpolates, mark markup you trust with raw(), or send plain text with res.text(body, { statusCode }).`);
18
+ };
4
19
  /**
5
20
  * Builds the responses of routes, hooks and error handlers. An API handler
6
21
  * never holds one: it returns its output or refuses, and writes headers and
@@ -52,23 +67,28 @@ export default class LambderResponseBuilder {
52
67
  json(data, options) {
53
68
  return this.buildResponse(200, "application/json; charset=utf-8", JSON.stringify(data), options);
54
69
  }
70
+ /** A plain-text body, sent as text: the place for a message, whatever status it goes out with (`{ statusCode }`). */
55
71
  text(data, options) {
56
72
  return this.buildResponse(200, "text/plain; charset=utf-8", data, options);
57
73
  }
74
+ /** An XML document (a sitemap, a feed, SVG) built with xml`...` or marked safe with raw(). */
58
75
  xml(data, options) {
59
- return this.buildResponse(200, "application/xml; charset=utf-8", String(data), options);
76
+ return this.buildResponse(200, "application/xml; charset=utf-8", markupBody("res.xml", "xml", data), options);
60
77
  }
61
78
  ;
79
+ /** An HTML page built with html`...` or marked safe with raw(). */
62
80
  html(data, options) {
63
- return this.buildResponse(200, "text/html; charset=utf-8", String(data), options);
81
+ return this.buildResponse(200, "text/html; charset=utf-8", markupBody("res.html", "html", data), options);
64
82
  }
65
83
  ;
84
+ /** An HTML response with any status code, its body built like res.html's; no body sends an empty one. */
66
85
  status(statusCode, body, options) {
67
- return this.buildResponse(statusCode, "text/html; charset=utf-8", body ?? "", options);
86
+ return this.buildResponse(statusCode, "text/html; charset=utf-8", body === undefined ? "" : markupBody("res.status", "html", body), options);
68
87
  }
69
88
  ;
89
+ /** An HTML 404, its body built like res.html's. */
70
90
  status404(data, options) {
71
- return this.buildResponse(404, "text/html; charset=utf-8", data, options);
91
+ return this.buildResponse(404, "text/html; charset=utf-8", markupBody("res.status404", "html", data), options);
72
92
  }
73
93
  ;
74
94
  /**
@@ -118,7 +138,7 @@ export default class LambderResponseBuilder {
118
138
  async file(filePath, options) {
119
139
  const file = await this.requireFiles("res.file").read(filePath);
120
140
  if (!file)
121
- return this.status404("File not found", { etag: false });
141
+ return this.text("File not found", { statusCode: 404, etag: false });
122
142
  return this.buildResponse(200, file.mimeType, file.body, options);
123
143
  }
124
144
  ;
@@ -127,12 +147,23 @@ export default class LambderResponseBuilder {
127
147
  * LambderTemplatingEngine (comment-based slots/conditionals) and return
128
148
  * it as an HTML response. The compiled template is cached on the
129
149
  * instance across warm invocations; a missing file throws (it is a
130
- * server-side configuration error, not a client 404). Set
131
- * htmlVirtualSlots to expose "title"/"head" slots on marker-less files.
150
+ * server-side configuration error, not a client 404), and so does a data
151
+ * key the file has no slot or condition for, naming the file. TNames, when
152
+ * given, is the file's slot and condition names, so a misspelled key is a
153
+ * compile error. Set htmlVirtualSlots to expose "title"/"head" slots on
154
+ * marker-less files.
132
155
  */
133
156
  async templateFile(filePath, data, options) {
134
157
  const template = await this.requireFiles("res.templateFile").template(filePath, { htmlVirtualSlots: options?.htmlVirtualSlots });
135
- return this.buildResponse(200, "text/html; charset=utf-8", template.render(data), options);
158
+ let body;
159
+ try {
160
+ body = template.render(data);
161
+ }
162
+ catch (error) {
163
+ // The engine knows its source, not the file it came from.
164
+ throw new Error(`Lambder: res.templateFile(${JSON.stringify(filePath)}): ${error.message}`, { cause: error });
165
+ }
166
+ return this.buildResponse(200, "text/html; charset=utf-8", body, options);
136
167
  }
137
168
  ;
138
169
  /**
@@ -1,5 +1,12 @@
1
1
  import { type LambderHtmlValue } from "../shared/LambderHtml.js";
2
- export type LambderTemplateData = Record<string, LambderHtmlValue>;
2
+ /**
3
+ * What a template renders with: a value per slot or condition name. TNames,
4
+ * when given, is the template's names, so a key outside them is a compile
5
+ * error; left out, any name is accepted here and checked at render.
6
+ */
7
+ export type LambderTemplateData<TNames extends string = string> = {
8
+ [Name in TNames]?: LambderHtmlValue;
9
+ };
3
10
  export type LambderTemplatingEngineOptions = {
4
11
  /**
5
12
  * For full HTML documents without declared markers: expose the <title>
@@ -8,18 +15,29 @@ export type LambderTemplatingEngineOptions = {
8
15
  */
9
16
  htmlVirtualSlots?: boolean;
10
17
  };
11
- export declare class LambderTemplatingEngine {
18
+ /**
19
+ * A comment-only HTML template, compiled once. TNames is the caller's
20
+ * statement of its slot and condition names, which types render()'s data;
21
+ * left out, any name compiles, and render() checks every key either way.
22
+ */
23
+ export declare class LambderTemplatingEngine<TNames extends string = string> {
12
24
  private nodes;
13
- /** Slot names discovered at compile time (dynamic typing surface). */
25
+ /** Every slot and condition name: what a data key has to be one of. */
26
+ private names;
27
+ /** Slot names discovered at compile time. */
14
28
  readonly slotNames: readonly string[];
15
29
  /** Condition names discovered at compile time. */
16
30
  readonly conditionNames: readonly string[];
17
31
  /** Parse `source`; throws on unclosed or mismatched blocks. */
18
32
  constructor(source: string, options?: LambderTemplatingEngineOptions);
19
33
  /** Read and parse a template file (compile once, render many times). */
20
- static fromFile(filePath: string, options?: LambderTemplatingEngineOptions): Promise<LambderTemplatingEngine>;
34
+ static fromFile<TNames extends string = string>(filePath: string, options?: LambderTemplatingEngineOptions): Promise<LambderTemplatingEngine<TNames>>;
21
35
  /** True when the template declares `name` as a slot or condition. */
22
36
  has(name: string): boolean;
23
- /** Render with escaped-by-default data; unknown keys ignored, omitted slots keep defaults. */
24
- render(data?: LambderTemplateData): string;
37
+ /**
38
+ * Render with escaped-by-default data; omitted slots keep their defaults.
39
+ * Throws for a key the template has no slot or condition for, naming it
40
+ * and the template's names.
41
+ */
42
+ render(data?: LambderTemplateData<TNames>): string;
25
43
  }