lambder 7.3.1 → 8.0.2

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 (207) hide show
  1. package/CHANGELOG.md +933 -3
  2. package/README.md +41 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +140 -75
  41. package/dist/core/Lambder.js +347 -227
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +21 -7
  51. package/dist/core/LambderFiles.js +62 -34
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  73. package/dist/invoke/LambderLambdaEvent.js +40 -22
  74. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  75. package/dist/invoke/lambderHandlerTransport.js +15 -18
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +1 -1
  93. package/dist/mock.js +2 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +124 -46
  99. package/dist/session/LambderSessionManager.js +262 -137
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  134. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  135. package/dist/shared/util/boundKeyField.d.ts +20 -0
  136. package/dist/shared/util/boundKeyField.js +34 -0
  137. package/dist/shared/util/canonicalJson.d.ts +11 -0
  138. package/dist/shared/util/canonicalJson.js +28 -0
  139. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  140. package/dist/shared/util/joinKeyFields.js +22 -0
  141. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  142. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  143. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  144. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  145. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  146. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  147. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  148. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  149. package/dist/shared/wire/LambderApiSignature.js +16 -19
  150. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  151. package/dist/shared/wire/LambderCallOptions.js +9 -11
  152. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  153. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  154. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  155. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  156. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  157. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  158. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  159. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  160. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  161. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  162. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  163. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  164. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  165. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  166. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  167. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  168. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  169. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  170. package/dist/stores/LambderCacheFiller.js +119 -0
  171. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  172. package/dist/stores/LambderCacheKeys.js +54 -0
  173. package/dist/stores/LambderCacheValues.d.ts +45 -0
  174. package/dist/stores/LambderCacheValues.js +74 -0
  175. package/dist/stores/LambderDdbCache.d.ts +121 -56
  176. package/dist/stores/LambderDdbCache.js +528 -225
  177. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  178. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  179. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  180. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  181. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  182. package/dist/stores/LambderDdbSdk.js +79 -33
  183. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  184. package/dist/stores/LambderDdbSessionStore.js +119 -47
  185. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  186. package/dist/stores/LambderHttpFileSource.js +15 -13
  187. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  188. package/dist/stores/LambderMemoryCache.js +113 -0
  189. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  190. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  191. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  192. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  193. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  194. package/dist/stores/LambderMemorySessionStore.js +38 -19
  195. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  196. package/dist/stores/LambderS3FileSource.js +12 -7
  197. package/dist/testing/LambderTestApp.d.ts +21 -23
  198. package/dist/testing/LambderTestApp.js +22 -24
  199. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  200. package/dist/testing/LambderTestVisitor.js +15 -15
  201. package/dist/testing.d.ts +1 -0
  202. package/dist/testing.js +1 -0
  203. package/package.json +12 -3
  204. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  205. package/dist/api/LambderApiPolicyEngine.js +0 -85
  206. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  207. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -2,17 +2,17 @@
2
2
  * Calling a Lambder app from another lambda, or from any server code that
3
3
  * holds AWS credentials and lambda:InvokeFunction on it.
4
4
  *
5
- * API Gateway delivers an HTTP request to a Lambder app as a JSON event and
6
- * takes a JSON response object back; a direct InvokeCommand carries JSON in
7
- * both directions too. So this caller builds the payload-format-2.0 event
8
- * API Gateway would have built, invokes the function with it, and reads the
9
- * response object Lambder returns. The callee is an unmodified Lambder app,
10
- * and everything it offers over HTTP (zod validation, the inferred contract,
11
- * refusals, guards, idempotency keys, Brotli answers, logList, the crash
12
- * detail its global error handler chooses to send) applies unchanged. The
13
- * callee tells an invoke from a browser only by the x-lambder-invoke header,
14
- * which is a marker for guards and hooks, never an authorization: the IAM
15
- * grant is that.
5
+ * Builds the payload-format-2.0 event API Gateway would have built, invokes
6
+ * the function with it directly, and reads the response object Lambder
7
+ * returns. The callee is an unmodified Lambder app, so everything it offers
8
+ * over HTTP (zod validation, the inferred contract, refusals, guards,
9
+ * idempotency keys, Brotli answers, logList, the crash detail its error
10
+ * handler chooses to send) applies unchanged. The callee tells an invoke
11
+ * apart by the event's requestContext.apiId, which no gateway lets a client
12
+ * write, and reads no forwarding header on one, so `clientIp` and `host` are
13
+ * the only address and host it sees. The x-lambder-invoke header is a marker
14
+ * for guards and hooks, never an authorization: the IAM grant is the
15
+ * authorization.
16
16
  *
17
17
  * Server-only (zlib, the Lambda SDK), so it is exported from the root entry
18
18
  * and never from lambder/client. The SDK is an optional peer dependency
@@ -25,11 +25,13 @@ import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionC
25
25
  import { readApiSignature } from "../shared/wire/LambderApiSignature.js";
26
26
  import { resolveApiOutcome } from "../shared/wire/LambderApiOutcome.js";
27
27
  import { mergeGuardInputs, } from "../shared/wire/LambderCallOptions.js";
28
+ import { beginIdempotentAttempt, IDEMPOTENT_ATTEMPT_NOT_SENT } from "../shared/wire/LambderIdempotencyKeyScope.js";
28
29
  import { createCallAbort, stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
29
30
  import { coerceToError, errorFromCrashDetail } from "../shared/wire/LambderCrashDetail.js";
30
31
  import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
31
32
  import { resolveCompressionOption } from "../shared/wire/LambderCompressionOption.js";
32
33
  import { DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, compressPayloadBrotli, resolveRequestCompressionMinBytes, } from "../shared/wire/LambderRequestPayload.js";
34
+ import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
33
35
  import { buildEnvelopeJson, decodeLambdaHttpResult, localLambdaContext, sessionCookies, synthesizeLambdaHttpEvent, } from "./LambderLambdaEvent.js";
34
36
  /**
35
37
  * Lambda caps a synchronous invoke's request and its response at about 6MB;
@@ -65,7 +67,7 @@ export default class LambderInvokeCaller {
65
67
  this.functionName = functionName;
66
68
  this.client = client;
67
69
  this.clientConfig = clientConfig;
68
- this.apiPath = apiPath ?? "/api";
70
+ this.apiPath = apiPath ?? DEFAULT_API_PATH;
69
71
  this.apiVersion = apiVersion;
70
72
  this.apiSignatures = apiSignatures;
71
73
  this.host = host ?? functionName;
@@ -88,9 +90,10 @@ export default class LambderInvokeCaller {
88
90
  const tokenCookieKey = init.sessionTokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY;
89
91
  return synthesizeLambdaHttpEvent({
90
92
  method: "POST",
91
- path: init.apiPath ?? "/api",
93
+ path: init.apiPath ?? DEFAULT_API_PATH,
92
94
  host,
93
95
  headers: init.headers,
96
+ contentType: "application/json",
94
97
  clientIp: init.clientIp,
95
98
  cookies: sessionCookies(init.session, tokenCookieKey),
96
99
  body: buildEnvelopeJson({
@@ -110,10 +113,9 @@ export default class LambderInvokeCaller {
110
113
  * Lambda would: a thrown error becomes a FunctionError payload. For
111
114
  * tests that want the real handlers behind the real envelope.
112
115
  *
113
- * It honours the signal the way lambderHandlerTransport does, by ending
114
- * the wait: a function call in this process cannot be cancelled, so the
115
- * handler runs to completion regardless and what a timeout buys is the
116
- * caller's answer. Ignoring it made timeoutMs a no-op here.
116
+ * It honours the signal by ending the wait, as lambderHandlerTransport
117
+ * does: an in-process call cannot be cancelled, so the handler runs to
118
+ * completion regardless, but timeoutMs still frees the caller.
117
119
  */
118
120
  static localTransport(handler, context = {}) {
119
121
  return async (event, { functionName, signal }) => {
@@ -148,9 +150,9 @@ export default class LambderInvokeCaller {
148
150
  }
149
151
  async invokeThroughSdk(eventJson, signal) {
150
152
  const { LambdaClient, InvokeCommand } = await this.loadSdk();
151
- // One call is one delivery attempt, which is what LambderApiTransport
152
- // promises: the SDK's own default of 3 would re-invoke a callee that
153
- // already ran when only the response was lost.
153
+ // One call is one delivery attempt, as LambderApiTransport promises:
154
+ // the SDK's default of 3 would re-invoke a callee that already ran
155
+ // when only the response was lost.
154
156
  if (!this.client)
155
157
  this.client = new LambdaClient({ maxAttempts: 1, ...this.clientConfig });
156
158
  const output = await this.client.send(new InvokeCommand({
@@ -172,10 +174,10 @@ export default class LambderInvokeCaller {
172
174
  }
173
175
  /** Delivers one event, serialized exactly once; an event over the invoke cap, a rejected transport, or one that answered after the call was given up on, is a failure. */
174
176
  async deliverEvent(event, eventJson, options) {
175
- // Measured here rather than on the API path alone, because every
176
- // caller of this one sends the same bytes. A path that skips the cap
177
- // gets the SDK's RequestEntityTooLargeException back instead, which
178
- // classifies as `protocol` and names neither the size nor the cap.
177
+ // Measured here so every path that delivers an event is capped: an
178
+ // oversized event would otherwise come back as the SDK's
179
+ // RequestEntityTooLargeException, which classifies as `protocol` and
180
+ // names neither the size nor the cap.
179
181
  const bytes = Buffer.byteLength(eventJson, "utf8");
180
182
  if (bytes > LAMBDER_INVOKE_MAX_EVENT_BYTES) {
181
183
  return { failed: {
@@ -188,16 +190,15 @@ export default class LambderInvokeCaller {
188
190
  const abort = createCallAbort({ timeoutMs: options.timeoutMs ?? this.timeoutMs, signal: options.signal });
189
191
  try {
190
192
  // A call the site has already given up on does not reach the
191
- // transport: honouring the signal is the transport's obligation
192
- // and not every transport does.
193
+ // transport: honouring the signal is the transport's obligation,
194
+ // and not every transport meets it.
193
195
  const refused = abort.abortFailure("beforeSending");
194
196
  if (refused)
195
197
  return { failed: { reason: refused.reason, cause: refused.error, detail: refused.error.message } };
196
198
  const sent = await this.transport(event, { functionName: this.functionName, eventJson, signal: abort.signal });
197
199
  // An answer that arrives after the abort is not a success: a
198
- // transport that ignores the signal resolves late, and believing
199
- // it would report ok on a 20ms timeoutMs at 300ms, handing the
200
- // call site data it had already abandoned.
200
+ // transport that ignores the signal resolves late, and trusting it
201
+ // would hand the call site data it had already abandoned.
201
202
  const late = abort.abortFailure("afterAnswering");
202
203
  if (late)
203
204
  return { failed: { reason: late.reason, cause: late.error, detail: late.error.message } };
@@ -205,8 +206,8 @@ export default class LambderInvokeCaller {
205
206
  }
206
207
  catch (err) {
207
208
  const cause = coerceToError(err, "the invoke failed");
208
- // The caller's own timeout wins, since only it knows about that;
209
- // otherwise the rejection says what it was.
209
+ // The caller's own timeout wins, since only the caller knows about
210
+ // it; otherwise the rejection says what it was.
210
211
  return { failed: { reason: abort.timedOut() ? 'timeout' : classifyDeliveryFailure(cause), cause } };
211
212
  }
212
213
  finally {
@@ -236,9 +237,8 @@ export default class LambderInvokeCaller {
236
237
  cause,
237
238
  });
238
239
  // FailureInit's arms mirror the outcome's, so each reason's evidence
239
- // was already demanded at the site that chose the reason; the
240
- // assembly is one object either way, and this is where it is named as
241
- // the arm it is rather than written out five times.
240
+ // was already demanded where the reason was chosen; the failure is
241
+ // assembled once here and cast to its arm.
242
242
  const failure = {
243
243
  ok: false,
244
244
  reason: init.reason,
@@ -282,15 +282,25 @@ export default class LambderInvokeCaller {
282
282
  for (const entry of logList)
283
283
  console.log(`[lambder invoke] ${this.functionName} ${apiName}`, entry);
284
284
  }
285
- /** One call, one outcome. Never throws; api() is what throws. */
285
+ /**
286
+ * One call, one outcome. Never throws; api() is what throws. A key scope
287
+ * is told how the attempt ended, as it is on LambderCaller.
288
+ */
286
289
  async dispatch(apiName, payload, options = {}) {
287
- // Everything that happens before the event leaves: the guardInputs
288
- // provider, the payload's JSON and its compression. All of it can
289
- // throw on the caller's own inputs (a provider that rejects, a
290
- // payload holding a cycle or a BigInt), and none of it may escape:
291
- // apiOutcome() promises an outcome, api() promises a
292
- // LambderInvokeError, and onFailure is the one place failures are
293
- // reported. So a throw here is an 'unknown' failure like any other.
290
+ const idempotentAttempt = beginIdempotentAttempt(options.idempotencyKey);
291
+ const outcome = await this.dispatchAttempt(apiName, payload, options, idempotentAttempt);
292
+ // Only the first settle counts: an attempt that never left settled
293
+ // itself as not sent.
294
+ idempotentAttempt.settle(outcome);
295
+ return outcome;
296
+ }
297
+ async dispatchAttempt(apiName, payload, options, idempotentAttempt) {
298
+ const idempotencyKey = idempotentAttempt.key;
299
+ // Everything before the event leaves can throw on the caller's own
300
+ // inputs (a provider that rejects, a payload holding a cycle or a
301
+ // BigInt). None of it may escape: apiOutcome() promises an outcome,
302
+ // api() a LambderInvokeError, and onFailure must see every failure,
303
+ // so it becomes an 'unknown' failure.
294
304
  let event;
295
305
  let eventJson;
296
306
  try {
@@ -303,10 +313,10 @@ export default class LambderInvokeCaller {
303
313
  ? await this.guardInputsProvider(apiName)
304
314
  : undefined;
305
315
  const guardInputs = mergeGuardInputs(provided, options.guardInputs);
306
- // The payload is serialized once: the compression decision needs
307
- // its JSON, and when it goes plainly that same JSON is spliced
308
- // into the envelope. Compressed when enabled and the JSON reaches
309
- // the threshold; `compressRequest` overrides both ways.
316
+ // Serialized once: the compression decision needs the JSON, and a
317
+ // plain payload splices that same JSON into the envelope.
318
+ // Compressed when enabled and the JSON reaches the threshold;
319
+ // `compressRequest` overrides both ways.
310
320
  const payloadJson = payload !== undefined ? JSON.stringify(payload) : undefined;
311
321
  const compressionMinBytes = resolveRequestCompressionMinBytes(options.compressRequest, this.requestCompression);
312
322
  const compressed = compressionMinBytes !== null && payloadJson !== undefined
@@ -317,6 +327,7 @@ export default class LambderInvokeCaller {
317
327
  path: this.apiPath,
318
328
  host: this.host,
319
329
  headers: options.headers,
330
+ contentType: "application/json",
320
331
  clientIp: options.clientIp,
321
332
  cookies: sessionCookies(options.session, this.sessionTokenCookieKey),
322
333
  body: buildEnvelopeJson({
@@ -328,13 +339,16 @@ export default class LambderInvokeCaller {
328
339
  payloadJson: compressed ? undefined : payloadJson,
329
340
  compressed,
330
341
  guardInputs,
331
- idempotencyKey: options.idempotencyKey,
342
+ idempotencyKey,
332
343
  }),
333
344
  }, { invoke: true });
334
345
  // Serialized once here; the size guard and the SDK transport both use it.
335
346
  eventJson = JSON.stringify(event);
336
347
  }
337
348
  catch (err) {
349
+ // Nothing was sent, so the key was not used: the same as the
350
+ // browser caller's failure before sending.
351
+ idempotentAttempt.settle(IDEMPOTENT_ATTEMPT_NOT_SENT);
338
352
  return await this.failureOutcome(apiName, { reason: 'unknown', cause: coerceToError(err, "the call could not be built") });
339
353
  }
340
354
  const delivery = await this.deliverEvent(event, eventJson, options);
@@ -357,17 +371,17 @@ export default class LambderInvokeCaller {
357
371
  json: async () => http.json(),
358
372
  text: async () => http.text(),
359
373
  });
360
- // Every answer's logs, from the one field the mapping puts them on:
361
- // an envelope's, a 500 body's, and a rejected input's, which the
362
- // callee writes onto the validation body as it does onto a success.
374
+ // Every answer's logs arrive on outcome.logList, whether they came
375
+ // from an envelope, a 500 body or a validation body.
363
376
  const logList = outcome.logList ?? [];
364
377
  await this.surfaceLogs(apiName, logList);
365
378
  // The answer's Set-Cookie values, so a session the callee rotated or
366
379
  // cleared is visible to whoever is carrying it.
367
380
  const cookies = http.cookies;
368
- // The declared output, by the callee's own typing: res.api(null) compiles
369
- // only for an output that allows null or beside a reason (an errorMessage
370
- // is a failure below; a message-only null is the callee's contract to keep).
381
+ // The declared output, by the callee's own typing: res.api(null)
382
+ // compiles only for an output that allows null or beside a reason (an
383
+ // errorMessage is a failure below; a message-only null is the
384
+ // callee's contract to keep).
371
385
  if (outcome.ok)
372
386
  return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList, cookies };
373
387
  const shared = { status: outcome.status, retryAfterSeconds: outcome.retryAfterSeconds, logList, cookies };
@@ -395,24 +409,21 @@ export default class LambderInvokeCaller {
395
409
  }
396
410
  return await this.failureOutcome(apiName, {
397
411
  ...shared,
398
- reason: outcome.reason,
399
- errorMessage: outcome.errorMessage,
412
+ ...(outcome.reason === 'errorMessage' ? { reason: outcome.reason, errorMessage: outcome.errorMessage } : { reason: outcome.reason }),
400
413
  response: outcome.response,
401
414
  crash: outcome.response.crash,
402
415
  });
403
416
  }
404
417
  /**
405
418
  * Full-fidelity call: resolves to a discriminated LambderInvokeOutcome
406
- * instead of throwing. Never throws; for sites that degrade gracefully.
407
- *
408
- * The output is computed from the contract in the return type rather than
409
- * taken as a type parameter, so a call site cannot replace it by
410
- * annotating what it assigns to.
419
+ * instead of throwing, for sites that degrade gracefully. The output type
420
+ * comes from the contract rather than a type parameter, so a call site
421
+ * cannot replace it by annotating what it assigns to.
411
422
  */
412
423
  async apiOutcome(apiName, ...rest) {
413
424
  // The tuple is a conditional type on an unresolved TApiName, so its
414
- // elements read as unknown from inside; the contract shaped them on
415
- // the way in, which is where the guarantee belongs.
425
+ // elements read as unknown here; the contract already checked them at
426
+ // the call site.
416
427
  const [payload, options] = rest;
417
428
  return await this.dispatch(apiName, payload, options);
418
429
  }
@@ -420,10 +431,9 @@ export default class LambderInvokeCaller {
420
431
  * The declared output, or a thrown LambderInvokeError carrying the
421
432
  * outcome. A failed dependency is a failed request: the throw reaches the
422
433
  * app's global error handler with the callee's error as its cause. The
423
- * result is the callee's output type as it declared it: the resolver
424
- * only lets a handler answer null when the output allows it or beside a
425
- * reason (LambderApiAnswer), so a nullable output is the one place null
426
- * arrives.
434
+ * resolver lets a handler answer null only when the output allows it or
435
+ * beside a reason (LambderApiAnswer), so null arrives only for a
436
+ * nullable output.
427
437
  */
428
438
  async api(apiName, ...rest) {
429
439
  const [payload, options] = rest;
@@ -4,16 +4,16 @@
4
4
  * rejected delivery means, what Lambda's error payload says, the one-line
5
5
  * detail an error message ends with).
6
6
  *
7
- * Split out of LambderInvokeCaller for the reason shared/wire/LambderApiOutcome.ts
8
- * is split out of the browser caller: this is the vocabulary a CALLER of the
7
+ * Kept apart from LambderInvokeCaller, as shared/wire/LambderApiOutcome.ts is
8
+ * kept apart from the browser caller: this is the vocabulary a CALLER of the
9
9
  * caller reads, and a site that only annotates an outcome or narrows an error
10
- * should not have to read a 700-line class to find it. The functions here
11
- * touch none of the caller's state, so every "what went wrong on an invoke"
12
- * answer is in one file.
10
+ * should not have to read the whole class to find it. None of these functions
11
+ * touch the caller's state.
13
12
  */
14
13
  import type { LambderApiEnvelopeBody } from "../shared/wire/LambderApiContract.js";
15
14
  import type { LambderApiFailureReason, LambderValidationError } from "../shared/wire/LambderApiOutcome.js";
16
15
  import type { LambderCrashDetail } from "../shared/wire/LambderCrashDetail.js";
16
+ import type { LambderAppRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
17
17
  export type LambderInvokeFailureReason = LambderApiFailureReason | 'crash' | 'protocol' | 'payloadTooLarge';
18
18
  /** Lambda's own error payload for a FunctionError invocation. */
19
19
  export type LambderInvokeFunctionError = {
@@ -26,8 +26,8 @@ type LambderInvokeFailureFields = {
26
26
  ok: false;
27
27
  /** HTTP status, when the callee answered. */
28
28
  status?: number;
29
- /** Envelope errorMessage, when the callee provided one. */
30
- errorMessage?: any;
29
+ /** Envelope errorMessage, when the callee provided one: always the message object, a plain string having been read as one (refusalMessageOf). */
30
+ errorMessage?: LambderAppRefusalMessage;
31
31
  /** Seconds to wait before retrying, from the answer's Retry-After header. */
32
32
  retryAfterSeconds?: number;
33
33
  /** Always present: the error api() throws for this failure, with the callee's error as its cause when one is known. */
@@ -53,13 +53,17 @@ export type LambderInvokePayloadTooLargeFailure = LambderInvokeFailureFields & {
53
53
  /** The event's byte size, so a caller can say by how much it is over. */
54
54
  bytes: number;
55
55
  };
56
- /** The callee answered, and the envelope itself says the call is refused. Always carries that envelope. */
56
+ /** The callee answered, and the envelope itself says the call is refused. Always carries that envelope, and an `errorMessage` refusal always carries its message. */
57
57
  export type LambderInvokeEnvelopeFailure = LambderInvokeFailureFields & {
58
- reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
59
58
  response: LambderApiEnvelopeBody<any>;
60
59
  /** The callee's crash detail, when its global error handler sent one (the envelope's `crash` field). */
61
60
  crash?: LambderCrashDetail;
62
- };
61
+ } & ({
62
+ reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized';
63
+ } | {
64
+ reason: 'errorMessage';
65
+ errorMessage: LambderAppRefusalMessage;
66
+ });
63
67
  /**
64
68
  * Nothing usable came back: the invoke never arrived or was given up on, the
65
69
  * Lambda service answered instead of the callee, the callee answered 5xx, or
@@ -74,13 +78,11 @@ export type LambderInvokeDeliveryFailure = LambderInvokeFailureFields & {
74
78
  crash?: LambderCrashDetail;
75
79
  };
76
80
  /**
77
- * A failed invoke, discriminated by `reason` rather than written as one arm of
78
- * optional fields, so narrowing to a reason narrows to what that reason
79
- * actually carries: `zodError` after `validation`, `functionError` after
81
+ * A failed invoke, discriminated by `reason` so narrowing to a reason narrows
82
+ * to what it carries: `zodError` after `validation`, `functionError` after
80
83
  * `crash`, `bytes` after `payloadTooLarge`, `response` after an envelope
81
- * reason, so a reader never writes an optional chain or a `!` for a field the
82
- * reason already guarantees. The browser caller's LambderApiOutcome is
83
- * discriminated the same way.
84
+ * reason, with no optional chain or `!` for a guaranteed field. The browser
85
+ * caller's LambderApiOutcome is discriminated the same way.
84
86
  *
85
87
  * `error`, `logList` and `cookies` are on every arm, and `status`,
86
88
  * `errorMessage` and `retryAfterSeconds` are there whenever an answer came
@@ -94,10 +96,10 @@ export type LambderInvokeOutcome<T> = {
94
96
  logList: unknown[];
95
97
  /**
96
98
  * The answer's Set-Cookie values. A caller carrying a user's session
97
- * is the browser for that call, and nothing else is: a callee that
98
- * rotated or cleared the session cookies says so here, and a caller
99
- * that ignores them keeps sending the old token. See
100
- * reissueSession() on the callee for the tokens themselves.
99
+ * is the browser for that call: a callee that rotated or cleared the
100
+ * session cookies says so here, and a caller that ignores them keeps
101
+ * sending the old token. See reissueSession() on the callee for the
102
+ * tokens themselves.
101
103
  */
102
104
  cookies: string[];
103
105
  } | LambderInvokeFailure;
@@ -107,7 +109,7 @@ export type LambderInvokeErrorInit = {
107
109
  apiName: string;
108
110
  functionName: string;
109
111
  status?: number;
110
- errorMessage?: any;
112
+ errorMessage?: LambderAppRefusalMessage;
111
113
  crash?: LambderCrashDetail;
112
114
  functionError?: LambderInvokeFunctionError;
113
115
  logList: unknown[];
@@ -130,7 +132,7 @@ export declare class LambderInvokeError extends Error {
130
132
  readonly apiName: string;
131
133
  readonly functionName: string;
132
134
  readonly status?: number;
133
- readonly errorMessage?: any;
135
+ readonly errorMessage?: LambderAppRefusalMessage;
134
136
  readonly crash?: LambderCrashDetail;
135
137
  readonly functionError?: LambderInvokeFunctionError;
136
138
  readonly logList: unknown[];
@@ -150,10 +152,9 @@ export declare const isLambderInvokeError: (err: unknown) => err is LambderInvok
150
152
  * SDK throws its service exceptions (AccessDeniedException,
151
153
  * ResourceNotFoundException, RequestEntityTooLargeException and the rest)
152
154
  * with a $fault mark and a name ending in "Exception", while a connectivity
153
- * failure arrives as a plain Error or TypeError carrying neither. The first
154
- * kind is the Lambda service answering the invoke, which is a permission,
155
- * wiring or size fault to go and fix; calling it `network` sent whoever read
156
- * it to look at their connection instead.
155
+ * failure is a plain Error or TypeError with neither. A service exception is
156
+ * a permission, wiring or size fault to fix; calling it `network` would send
157
+ * the reader to check their connection instead.
157
158
  */
158
159
  export declare const classifyDeliveryFailure: (error: Error) => "network" | "protocol";
159
160
  /** Lambda's error payload as an Error, with the callee's own name and trace. */
@@ -4,12 +4,11 @@
4
4
  * rejected delivery means, what Lambda's error payload says, the one-line
5
5
  * detail an error message ends with).
6
6
  *
7
- * Split out of LambderInvokeCaller for the reason shared/wire/LambderApiOutcome.ts
8
- * is split out of the browser caller: this is the vocabulary a CALLER of the
7
+ * Kept apart from LambderInvokeCaller, as shared/wire/LambderApiOutcome.ts is
8
+ * kept apart from the browser caller: this is the vocabulary a CALLER of the
9
9
  * caller reads, and a site that only annotates an outcome or narrows an error
10
- * should not have to read a 700-line class to find it. The functions here
11
- * touch none of the caller's state, so every "what went wrong on an invoke"
12
- * answer is in one file.
10
+ * should not have to read the whole class to find it. None of these functions
11
+ * touch the caller's state.
13
12
  */
14
13
  import { isLambderTransportFailure } from "../shared/transport/LambderApiTransport.js";
15
14
  /**
@@ -63,10 +62,9 @@ export const isLambderInvokeError = (err) => err instanceof Error && err.isLambd
63
62
  * SDK throws its service exceptions (AccessDeniedException,
64
63
  * ResourceNotFoundException, RequestEntityTooLargeException and the rest)
65
64
  * with a $fault mark and a name ending in "Exception", while a connectivity
66
- * failure arrives as a plain Error or TypeError carrying neither. The first
67
- * kind is the Lambda service answering the invoke, which is a permission,
68
- * wiring or size fault to go and fix; calling it `network` sent whoever read
69
- * it to look at their connection instead.
65
+ * failure is a plain Error or TypeError with neither. A service exception is
66
+ * a permission, wiring or size fault to fix; calling it `network` would send
67
+ * the reader to check their connection instead.
70
68
  */
71
69
  export const classifyDeliveryFailure = (error) => {
72
70
  if (isLambderTransportFailure(error))
@@ -102,19 +100,8 @@ export const describeFailure = (init) => {
102
100
  return init.crash.message;
103
101
  if (init.functionError)
104
102
  return `${init.functionError.errorType ?? "FunctionError"}: ${init.functionError.errorMessage ?? "the function failed"}`;
105
- if (init.errorMessage !== undefined) {
106
- const content = init.errorMessage?.content;
107
- if (typeof content === "string")
108
- return content;
109
- if (typeof init.errorMessage === "string")
110
- return init.errorMessage;
111
- try {
112
- return JSON.stringify(init.errorMessage);
113
- }
114
- catch {
115
- return String(init.errorMessage);
116
- }
117
- }
103
+ if (init.errorMessage !== undefined)
104
+ return init.errorMessage.content;
118
105
  if (init.reason === 'validation')
119
106
  return "the callee rejected the input";
120
107
  if (init.reason === 'versionExpired')
@@ -11,7 +11,12 @@
11
11
  import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, Context } from "aws-lambda";
12
12
  import type { LambderHttpEventFormat } from "../core/LambderContext.js";
13
13
  import type { LambderCompressedBrotliPayload, LambderCompressedGzipPayload } from "../shared/wire/LambderRequestPayload.js";
14
- /** Marks a synthesized request as an invoke, for guards and hooks that want to tell. Not an authorization. */
14
+ /**
15
+ * Marks a synthesized request as an invoke, for guards and hooks that want to
16
+ * tell. Not an authorization: over HTTP it is a header any client can send.
17
+ * The server itself tells an invoke by its requestContext.apiId, which no
18
+ * gateway lets a client write (see LAMBDER_INVOKE_API_ID).
19
+ */
15
20
  export declare const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
16
21
  /** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
17
22
  export declare const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
@@ -33,21 +38,36 @@ export type LambderSynthesizedRequest = {
33
38
  clientIp?: string;
34
39
  cookies?: string[];
35
40
  body?: string | Buffer;
41
+ /**
42
+ * The body's type, owned by the event over any Content-Type in `headers`:
43
+ * an API call's envelope is JSON whatever headers a caller forwards, and
44
+ * a server takes a POST to its API path as an API call only when it says
45
+ * so. Left out, a caller's own Content-Type stands, and a body without
46
+ * one is typed by its kind.
47
+ */
48
+ contentType?: string;
36
49
  };
37
50
  /**
38
51
  * The event API Gateway would deliver for this request: payload format 2.0
39
- * (an HTTP API, a Function URL) unless `eventFormat: "v1"` asks for the REST
40
- * API's. An invoke is always 2.0; the other format is for an in-process call
41
- * that wants the handler to meet the shape its own deployment delivers.
52
+ * (an HTTP API's, whose path arrives decoded) unless `eventFormat: "v1"` asks
53
+ * for the REST API's, whose path arrives as written. An invoke is always 2.0;
54
+ * the other format is for an in-process call that wants the handler to meet
55
+ * the shape its own deployment delivers.
42
56
  * `invoke: true` adds the invoke marker headers a server-to-server call
43
57
  * carries; a browser-shaped request (the handler transport) leaves them off.
44
58
  *
45
59
  * The client address is `clientIp` and reaches the callee as the gateway's
46
- * observed source address only (requestContext.http.sourceIp, or
47
- * requestContext.identity.sourceIp on a REST API event). Writing it as
48
- * x-forwarded-for as well would put the same fact on a channel a callee may
49
- * be configured to trust (trustedClientIpHeaders), and the header is the one
50
- * the caller's own `headers` could otherwise have set.
60
+ * observed source address (requestContext.http.sourceIp, or
61
+ * requestContext.identity.sourceIp on a REST API event). On an invoke it is
62
+ * the only channel, and x-forwarded-for is dropped from the caller's
63
+ * `headers`: a server of this version reads no trusted forwarding header on
64
+ * an event carrying LAMBDER_INVOKE_API_ID, but a callee on Lambder 7.x reads
65
+ * the one it trusts on any event, so a gateway lambda forwarding a browser's
66
+ * headers would hand it a ctx.ip the browser chose. x-forwarded-for is the
67
+ * header a gateway writes and the one such a callee trusts in the common
68
+ * case. A browser-shaped request keeps every header it was given: it stands
69
+ * for what a gateway delivered, and a test that writes a forwarding header
70
+ * on one is exercising the app's own trustedClientIpHeaders.
51
71
  */
52
72
  export declare function synthesizeLambdaHttpEvent(request: LambderSynthesizedRequest, options: {
53
73
  invoke: boolean;