lambder 11.1.1 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +25 -15
  3. package/dist/api/LambderApiAnswer.d.ts +7 -0
  4. package/dist/api/LambderApiDeclarations.d.ts +194 -0
  5. package/dist/api/LambderApiDeclarations.js +52 -0
  6. package/dist/api/LambderApiDefinition.d.ts +1 -1
  7. package/dist/api/LambderApiEnvelope.js +4 -1
  8. package/dist/api/LambderApiGuards.d.ts +21 -22
  9. package/dist/api/LambderApiGuards.js +1 -1
  10. package/dist/api/LambderApiRateLimits.d.ts +19 -19
  11. package/dist/api/LambderApiRateLimits.js +1 -1
  12. package/dist/api/LambderApiRefusals.d.ts +22 -0
  13. package/dist/api/LambderApiRefusals.js +13 -0
  14. package/dist/api/LambderApiRequest.d.ts +15 -6
  15. package/dist/api/LambderApiRequest.js +8 -9
  16. package/dist/build/generateApiFiles.d.ts +60 -0
  17. package/dist/build/generateApiFiles.js +85 -0
  18. package/dist/build/generatedTables.d.ts +2 -1
  19. package/dist/build/generatedTables.js +1 -1
  20. package/dist/build.d.ts +4 -1
  21. package/dist/build.js +3 -1
  22. package/dist/client/LambderCaller.d.ts +36 -23
  23. package/dist/client/LambderCaller.js +88 -67
  24. package/dist/client/lambderFetchTransport.js +2 -1
  25. package/dist/client.d.ts +3 -1
  26. package/dist/client.js +2 -0
  27. package/dist/core/Lambder.d.ts +232 -92
  28. package/dist/core/Lambder.js +400 -103
  29. package/dist/core/LambderCallSummary.d.ts +43 -0
  30. package/dist/core/LambderCallSummary.js +10 -0
  31. package/dist/core/LambderContext.d.ts +15 -2
  32. package/dist/core/LambderContext.js +32 -9
  33. package/dist/core/LambderCreateOptions.d.ts +78 -64
  34. package/dist/core/LambderCreateOptions.js +39 -21
  35. package/dist/core/LambderInvocationScope.d.ts +25 -0
  36. package/dist/core/LambderInvocationScope.js +21 -0
  37. package/dist/core/LambderResponse.d.ts +11 -0
  38. package/dist/core/LambderResponse.js +12 -0
  39. package/dist/core/LambderResponseBuilder.js +5 -1
  40. package/dist/index.d.ts +13 -7
  41. package/dist/index.js +3 -1
  42. package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
  43. package/dist/invoke/LambderInvokeCaller.js +110 -94
  44. package/dist/invoke/LambderLambdaEvent.d.ts +2 -1
  45. package/dist/invoke/LambderLambdaEvent.js +8 -3
  46. package/dist/invoke/lambderHandlerTransport.js +2 -1
  47. package/dist/mock/LambderMockApp.d.ts +39 -33
  48. package/dist/mock/LambderMockApp.js +66 -55
  49. package/dist/mock/LambderMockCreateOptions.d.ts +8 -10
  50. package/dist/mock/LambderMockTypes.d.ts +44 -7
  51. package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
  52. package/dist/mock/lambderMockInvokeTransport.js +15 -7
  53. package/dist/mock/lambderMockMswHandler.js +8 -4
  54. package/dist/mock.d.ts +2 -2
  55. package/dist/session/LambderSessionManager.d.ts +46 -3
  56. package/dist/session/LambderSessionManager.js +62 -11
  57. package/dist/shared/LambderI18n.js +49 -4
  58. package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
  59. package/dist/shared/contracts/LambderRateLimiter.js +2 -2
  60. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  61. package/dist/shared/transport/LambderApiTransport.js +2 -3
  62. package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
  63. package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
  64. package/dist/shared/util/LambderInvocationScope.js +5 -0
  65. package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
  66. package/dist/shared/util/LambderNamedMaps.js +19 -0
  67. package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
  68. package/dist/shared/wire/LambderApiContract.d.ts +4 -18
  69. package/dist/shared/wire/LambderApiContract.js +1 -1
  70. package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
  71. package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
  72. package/dist/shared/wire/LambderApiNames.d.ts +55 -0
  73. package/dist/shared/wire/LambderApiNames.js +81 -0
  74. package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
  75. package/dist/shared/wire/LambderCallOutcome.js +47 -0
  76. package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
  77. package/dist/shared/wire/LambderInvokeApiId.js +10 -0
  78. package/dist/shared/wire/LambderUploadSchemas.js +2 -1
  79. package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
  80. package/dist/stores/LambderDdbRateLimiter.js +64 -45
  81. package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
  82. package/dist/stores/LambderMemoryRateLimiter.js +20 -15
  83. package/dist/testing/LambderTestApp.d.ts +18 -5
  84. package/dist/testing/LambderTestApp.js +21 -5
  85. package/dist/testing/LambderTestVisitor.d.ts +16 -8
  86. package/dist/testing/LambderTestVisitor.js +33 -25
  87. package/package.json +1 -1
@@ -20,6 +20,8 @@
20
20
  * LambderInvokeCaller.localTransport runs a callee's handler in-process for
21
21
  * tests.
22
22
  */
23
+ import { apiCallPath } from "../shared/wire/LambderApiNames.js";
24
+ import { withApiGroupCalls } from "../shared/wire/LambderApiGroupCalls.js";
23
25
  import { classifyDeliveryFailure, describeFailure, errorFromFunctionError, LambderInvokeError, parseFunctionError, } from "./LambderInvokeOutcome.js";
24
26
  import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
25
27
  import { readApiSignature } from "../shared/wire/LambderApiSignatureMap.js";
@@ -40,46 +42,57 @@ import { buildEnvelopeJson, decodeLambdaHttpResult, localLambdaContext, sessionC
40
42
  */
41
43
  export const LAMBDER_INVOKE_MAX_EVENT_BYTES = 5_500_000;
42
44
  /**
45
+ * The invoke caller itself, before the groups: what LambderInvokeCaller is,
46
+ * less the callee's endpoints by group (LambderInvokeGroupCalls), which the
47
+ * constructor adds.
48
+ *
43
49
  * @typeParam TContract - The callee's API contract (`typeof lambder.ApiContract`, imported type-only), for typed names, payloads, results and guard inputs.
44
50
  * @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
45
51
  */
46
- export default class LambderInvokeCaller {
47
- functionName;
48
- apiPath;
49
- apiVersion;
50
- apiSignatures;
51
- host;
52
- requestCompression;
53
- maxResponsePayloadBytes;
54
- timeoutMs;
55
- onLogList;
56
- onFailure;
57
- guardInputsProvider;
58
- sessionTokenCookieKey;
59
- transport;
60
- clientConfig;
61
- client;
62
- sdk;
52
+ class LambderInvokeCallerCore {
53
+ #functionName;
54
+ #apiPath;
55
+ #apiVersion;
56
+ #apiSignatures;
57
+ #host;
58
+ #requestCompression;
59
+ #maxResponsePayloadBytes;
60
+ #timeoutMs;
61
+ #onLogList;
62
+ #onFailure;
63
+ #beforeCall;
64
+ #guardInputsProvider;
65
+ #sessionTokenCookieKey;
66
+ #transport;
67
+ #clientConfig;
68
+ #client;
69
+ #sdk;
63
70
  constructor(options) {
64
- const { functionName, client, clientConfig, apiPath, apiVersion, apiSignatures, host, requestCompression, maxResponsePayloadBytes, timeoutMs, onLogList, onFailure, sessionTokenCookieKey, transport, guardInputsProvider, } = options;
71
+ const { functionName, client, clientConfig, apiPath, apiVersion, apiSignatures, host, requestCompression, maxResponsePayloadBytes, timeoutMs, onLogList, onFailure, beforeCall, sessionTokenCookieKey, transport, guardInputsProvider, } = options;
65
72
  if (!functionName?.trim())
66
73
  throw new Error("LambderInvokeCaller: functionName is required");
67
- this.functionName = functionName;
68
- this.client = client;
69
- this.clientConfig = clientConfig;
70
- this.apiPath = apiPath ?? DEFAULT_API_PATH;
71
- this.apiVersion = apiVersion;
72
- this.apiSignatures = apiSignatures;
73
- this.host = host ?? functionName;
74
+ this.#functionName = functionName;
75
+ this.#client = client;
76
+ this.#clientConfig = clientConfig;
77
+ this.#apiPath = apiPath ?? DEFAULT_API_PATH;
78
+ this.#apiVersion = apiVersion;
79
+ this.#apiSignatures = apiSignatures;
80
+ this.#host = host ?? functionName;
74
81
  // `?? false`: like the browser caller, off unless asked for.
75
- this.requestCompression = resolveCompressionOption(requestCompression ?? false, DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS);
76
- this.maxResponsePayloadBytes = assertPositiveInteger(maxResponsePayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "LambderInvokeCaller maxResponsePayloadBytes");
77
- this.timeoutMs = timeoutMs;
78
- this.onLogList = onLogList;
79
- this.onFailure = onFailure;
80
- this.guardInputsProvider = guardInputsProvider;
81
- this.sessionTokenCookieKey = sessionTokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY;
82
- this.transport = transport ?? ((_event, { eventJson, signal }) => this.invokeThroughSdk(eventJson, signal));
82
+ this.#requestCompression = resolveCompressionOption(requestCompression ?? false, DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS);
83
+ this.#maxResponsePayloadBytes = assertPositiveInteger(maxResponsePayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "LambderInvokeCaller maxResponsePayloadBytes");
84
+ this.#timeoutMs = timeoutMs;
85
+ this.#onLogList = onLogList;
86
+ this.#onFailure = onFailure;
87
+ this.#beforeCall = beforeCall;
88
+ this.#guardInputsProvider = guardInputsProvider;
89
+ this.#sessionTokenCookieKey = sessionTokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY;
90
+ this.#transport = transport ?? ((_event, { eventJson, signal }) => this.#invokeThroughSdk(eventJson, signal));
91
+ // Each group of the callee's contract, as a property:
92
+ // caller.email.send(input) is caller.api("email.send", input), and
93
+ // .outcome the apiOutcome.
94
+ const byName = this;
95
+ return withApiGroupCalls(this, (apiName, args) => byName.api(apiName, ...args), (apiName, args) => byName.apiOutcome(apiName, ...args));
83
96
  }
84
97
  /**
85
98
  * The event api() would send for this call, with a plain payload. For
@@ -90,14 +103,13 @@ export default class LambderInvokeCaller {
90
103
  const tokenCookieKey = init.sessionTokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY;
91
104
  return synthesizeLambdaHttpEvent({
92
105
  method: "POST",
93
- path: init.apiPath ?? DEFAULT_API_PATH,
106
+ path: apiCallPath(init.apiPath ?? DEFAULT_API_PATH, init.apiName),
94
107
  host,
95
108
  headers: init.headers,
96
109
  contentType: "application/json",
97
110
  clientIp: init.clientIp,
98
111
  cookies: sessionCookies(init.session, tokenCookieKey),
99
112
  body: buildEnvelopeJson({
100
- apiName: init.apiName,
101
113
  version: init.apiVersion,
102
114
  signature: init.signature,
103
115
  csrf: init.session?.csrf,
@@ -140,23 +152,23 @@ export default class LambderInvokeCaller {
140
152
  }
141
153
  };
142
154
  }
143
- loadSdk() {
144
- if (!this.sdk) {
145
- this.sdk = import("@aws-sdk/client-lambda").catch(() => {
155
+ #loadSdk() {
156
+ if (!this.#sdk) {
157
+ this.#sdk = import("@aws-sdk/client-lambda").catch(() => {
146
158
  throw new Error("LambderInvokeCaller requires @aws-sdk/client-lambda: npm install @aws-sdk/client-lambda");
147
159
  });
148
160
  }
149
- return this.sdk;
161
+ return this.#sdk;
150
162
  }
151
- async invokeThroughSdk(eventJson, signal) {
152
- const { LambdaClient, InvokeCommand } = await this.loadSdk();
163
+ async #invokeThroughSdk(eventJson, signal) {
164
+ const { LambdaClient, InvokeCommand } = await this.#loadSdk();
153
165
  // One call is one delivery attempt, as LambderApiTransport promises:
154
166
  // the SDK's default of 3 would re-invoke a callee that already ran
155
167
  // when only the response was lost.
156
- if (!this.client)
157
- this.client = new LambdaClient({ maxAttempts: 1, ...this.clientConfig });
158
- const output = await this.client.send(new InvokeCommand({
159
- FunctionName: this.functionName,
168
+ if (!this.#client)
169
+ this.#client = new LambdaClient({ maxAttempts: 1, ...this.#clientConfig });
170
+ const output = await this.#client.send(new InvokeCommand({
171
+ FunctionName: this.#functionName,
160
172
  InvocationType: "RequestResponse",
161
173
  Payload: Buffer.from(eventJson, "utf8"),
162
174
  }), signal ? { abortSignal: signal } : undefined);
@@ -173,7 +185,7 @@ export default class LambderInvokeCaller {
173
185
  return { functionError: output.FunctionError ?? null, result };
174
186
  }
175
187
  /** 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. */
176
- async deliverEvent(event, eventJson, options) {
188
+ async #deliverEvent(event, eventJson, options) {
177
189
  // Measured here so every path that delivers an event is capped: an
178
190
  // oversized event would otherwise come back as the SDK's
179
191
  // RequestEntityTooLargeException, which classifies as `protocol` and
@@ -187,7 +199,7 @@ export default class LambderInvokeCaller {
187
199
  }
188
200
  // The same wiring the browser caller uses, so the two cannot drift on
189
201
  // what a late or abandoned call means.
190
- const abort = createCallAbort({ timeoutMs: options.timeoutMs ?? this.timeoutMs, signal: options.signal });
202
+ const abort = createCallAbort({ timeoutMs: options.timeoutMs ?? this.#timeoutMs, signal: options.signal });
191
203
  try {
192
204
  // A call the site has already given up on does not reach the
193
205
  // transport: honouring the signal is the transport's obligation,
@@ -195,7 +207,7 @@ export default class LambderInvokeCaller {
195
207
  const refused = abort.abortFailure("beforeSending");
196
208
  if (refused)
197
209
  return { failed: { reason: refused.reason, cause: refused.error, detail: refused.error.message } };
198
- const sent = await this.transport(event, { functionName: this.functionName, eventJson, signal: abort.signal });
210
+ const sent = await this.#transport(event, { functionName: this.#functionName, eventJson, signal: abort.signal });
199
211
  // An answer that arrives after the abort is not a success: a
200
212
  // transport that ignores the signal resolves late, and trusting it
201
213
  // would hand the call site data it had already abandoned.
@@ -215,17 +227,17 @@ export default class LambderInvokeCaller {
215
227
  }
216
228
  }
217
229
  /** Builds the failure and its error, reports it once, and hands it back. */
218
- async failureOutcome(apiName, init) {
230
+ async #failureOutcome(apiName, init) {
219
231
  const logList = init.logList ?? [];
220
232
  const cause = init.crash ? errorFromCrashDetail(init.crash)
221
233
  : init.functionError ? errorFromFunctionError(init.functionError)
222
234
  : init.cause;
223
235
  const detail = init.detail ?? describeFailure(init);
224
236
  const error = new LambderInvokeError({
225
- message: `${this.functionName} ${apiName} failed (${init.reason}): ${detail}`,
237
+ message: `${this.#functionName} ${apiName} failed (${init.reason}): ${detail}`,
226
238
  reason: init.reason,
227
239
  apiName,
228
- functionName: this.functionName,
240
+ functionName: this.#functionName,
229
241
  status: init.status,
230
242
  refusal: init.refusal,
231
243
  crash: init.crash,
@@ -255,49 +267,51 @@ export default class LambderInvokeCaller {
255
267
  ...(init.response !== undefined ? { response: init.response } : {}),
256
268
  };
257
269
  error.outcome = failure;
258
- if (this.onFailure) {
270
+ if (this.#onFailure) {
259
271
  // A reporting hook that breaks must not turn apiOutcome() into a
260
272
  // throwing call, nor replace the failure it was told about.
261
273
  try {
262
- await this.onFailure(failure, { apiName, functionName: this.functionName });
274
+ await this.#onFailure(failure, { apiName, functionName: this.#functionName });
263
275
  }
264
276
  catch (err) {
265
- console.error(`[lambder invoke] onFailure threw for ${this.functionName} ${apiName}`, err);
277
+ console.error(`[lambder invoke] onFailure threw for ${this.#functionName} ${apiName}`, err);
266
278
  }
267
279
  }
268
280
  return failure;
269
281
  }
270
- async surfaceLogs(apiName, logList) {
282
+ async #surfaceLogs(apiName, logList) {
271
283
  if (!logList.length)
272
284
  return;
273
- if (this.onLogList) {
285
+ if (this.#onLogList) {
274
286
  try {
275
- await this.onLogList(apiName, logList);
287
+ await this.#onLogList(apiName, logList);
276
288
  }
277
289
  catch (err) {
278
- console.error(`[lambder invoke] onLogList threw for ${this.functionName} ${apiName}`, err);
290
+ console.error(`[lambder invoke] onLogList threw for ${this.#functionName} ${apiName}`, err);
279
291
  }
280
292
  return;
281
293
  }
282
294
  for (const entry of logList)
283
- console.log(`[lambder invoke] ${this.functionName} ${apiName}`, entry);
295
+ console.log(`[lambder invoke] ${this.#functionName} ${apiName}`, entry);
284
296
  }
285
297
  /**
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.
298
+ * One call, one outcome. Never throws, but for what beforeCall throws;
299
+ * api() is what throws. A key scope is told how the attempt ended, as it
300
+ * is on LambderCaller.
288
301
  */
289
- async dispatch(apiName, payload, options = {}) {
302
+ async #dispatch(apiName, payload, options = {}) {
303
+ this.#beforeCall?.(apiName, { functionName: this.#functionName });
290
304
  const idempotentAttempt = beginIdempotentAttempt(options.idempotencyKey);
291
305
  // The reader cannot check a refusal's code against declarations it
292
306
  // does not have; the callee never sends one its endpoint did not
293
307
  // declare, which is what the endpoint's message type stands for.
294
- const outcome = await this.dispatchAttempt(apiName, payload, options, idempotentAttempt);
308
+ const outcome = await this.#dispatchAttempt(apiName, payload, options, idempotentAttempt);
295
309
  // Only the first settle counts: an attempt that never left settled
296
310
  // itself as not sent.
297
311
  idempotentAttempt.settle(outcome);
298
312
  return outcome;
299
313
  }
300
- async dispatchAttempt(apiName, payload, options, idempotentAttempt) {
314
+ async #dispatchAttempt(apiName, payload, options, idempotentAttempt) {
301
315
  const idempotencyKey = idempotentAttempt.key;
302
316
  // Everything before the event leaves can throw on the caller's own
303
317
  // inputs (a provider that rejects, a payload holding a cycle or a
@@ -310,10 +324,10 @@ export default class LambderInvokeCaller {
310
324
  // The callee's signature for this endpoint, when this caller was
311
325
  // built with the callee's map. A name the map lacks fails here,
312
326
  // as a provider that threw would: the map predates the endpoint.
313
- const signature = this.apiSignatures ? await readApiSignature(this.apiSignatures, apiName) : undefined;
327
+ const signature = this.#apiSignatures ? await readApiSignature(this.#apiSignatures, apiName) : undefined;
314
328
  // Provider values underneath, per-call values on top.
315
- const provided = this.guardInputsProvider
316
- ? await this.guardInputsProvider(apiName)
329
+ const provided = this.#guardInputsProvider
330
+ ? await this.#guardInputsProvider(apiName)
317
331
  : undefined;
318
332
  const guardInputs = mergeGuardInputs(provided, options.guardInputs);
319
333
  // Serialized once: the compression decision needs the JSON, and a
@@ -321,24 +335,23 @@ export default class LambderInvokeCaller {
321
335
  // Compressed when enabled and the JSON reaches the threshold;
322
336
  // `compressRequest` overrides both ways.
323
337
  const payloadJson = payload !== undefined ? JSON.stringify(payload) : undefined;
324
- const compressionMinBytes = resolveRequestCompressionMinBytes(options.compressRequest, this.requestCompression);
338
+ const compressionMinBytes = resolveRequestCompressionMinBytes(options.compressRequest, this.#requestCompression);
325
339
  const compressed = compressionMinBytes !== null && payloadJson !== undefined
326
- ? await compressPayloadBrotli(payloadJson, compressionMinBytes, this.requestCompression?.quality ?? DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS.quality)
340
+ ? await compressPayloadBrotli(payloadJson, compressionMinBytes, this.#requestCompression?.quality ?? DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS.quality)
327
341
  : null;
328
342
  event = synthesizeLambdaHttpEvent({
329
343
  method: "POST",
330
- path: this.apiPath,
331
- host: this.host,
344
+ path: apiCallPath(this.#apiPath, apiName),
345
+ host: this.#host,
332
346
  headers: options.headers,
333
347
  contentType: "application/json",
334
348
  clientIp: options.clientIp,
335
- cookies: sessionCookies(options.session, this.sessionTokenCookieKey),
349
+ cookies: sessionCookies(options.session, this.#sessionTokenCookieKey),
336
350
  body: buildEnvelopeJson({
337
- apiName,
338
- version: this.apiVersion,
351
+ version: this.#apiVersion,
339
352
  signature,
340
353
  csrf: options.session?.csrf,
341
- siteHost: this.host,
354
+ siteHost: this.#host,
342
355
  payloadJson: compressed ? undefined : payloadJson,
343
356
  compressed,
344
357
  guardInputs,
@@ -352,21 +365,21 @@ export default class LambderInvokeCaller {
352
365
  // Nothing was sent, so the key was not used: the same as the
353
366
  // browser caller's failure before sending.
354
367
  idempotentAttempt.settle(IDEMPOTENT_ATTEMPT_NOT_SENT);
355
- return await this.failureOutcome(apiName, { reason: 'unknown', cause: coerceToError(err, "the call could not be built") });
368
+ return await this.#failureOutcome(apiName, { reason: 'unknown', cause: coerceToError(err, "the call could not be built") });
356
369
  }
357
- const delivery = await this.deliverEvent(event, eventJson, options);
370
+ const delivery = await this.#deliverEvent(event, eventJson, options);
358
371
  if ("failed" in delivery)
359
- return await this.failureOutcome(apiName, delivery.failed);
372
+ return await this.#failureOutcome(apiName, delivery.failed);
360
373
  if (delivery.sent.functionError) {
361
- return await this.failureOutcome(apiName, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) });
374
+ return await this.#failureOutcome(apiName, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) });
362
375
  }
363
376
  let http;
364
377
  try {
365
- http = await decodeLambdaHttpResult(delivery.sent.result, this.maxResponsePayloadBytes);
378
+ http = await decodeLambdaHttpResult(delivery.sent.result, this.#maxResponsePayloadBytes);
366
379
  }
367
380
  catch (err) {
368
381
  const cause = coerceToError(err, "the answer could not be decoded");
369
- return await this.failureOutcome(apiName, { reason: 'protocol', cause, detail: cause.message });
382
+ return await this.#failureOutcome(apiName, { reason: 'protocol', cause, detail: cause.message });
370
383
  }
371
384
  const outcome = await resolveApiOutcome({
372
385
  status: http.statusCode,
@@ -377,7 +390,7 @@ export default class LambderInvokeCaller {
377
390
  // Every answer's logs arrive on outcome.logList, whether they came
378
391
  // from an envelope, a 500 body or a validation body.
379
392
  const logList = outcome.logList ?? [];
380
- await this.surfaceLogs(apiName, logList);
393
+ await this.#surfaceLogs(apiName, logList);
381
394
  // The answer's Set-Cookie values, so a session the callee rotated or
382
395
  // cleared is visible to whoever is carrying it.
383
396
  const cookies = http.cookies;
@@ -390,15 +403,15 @@ export default class LambderInvokeCaller {
390
403
  // union says which: a rejected input has its issues and no envelope,
391
404
  // an envelope refusal has the envelope and no Error.
392
405
  if (outcome.reason === 'validation') {
393
- return await this.failureOutcome(apiName, { ...shared, reason: 'validation', zodError: outcome.zodError });
406
+ return await this.#failureOutcome(apiName, { ...shared, reason: 'validation', zodError: outcome.zodError });
394
407
  }
395
408
  if (outcome.reason === 'server') {
396
409
  // A 404 text page is what a callee answers when apiPath does not
397
410
  // match: the one misconfiguration every first integration hits.
398
411
  const detail = http.statusCode === 404
399
- ? `no API at ${this.apiPath} on ${this.functionName} (HTTP 404): does apiPath match the callee's?`
412
+ ? `no API at ${this.#apiPath} on ${this.#functionName} (HTTP 404): does apiPath match the callee's?`
400
413
  : undefined;
401
- return await this.failureOutcome(apiName, {
414
+ return await this.#failureOutcome(apiName, {
402
415
  ...shared,
403
416
  reason: 'server',
404
417
  refusal: outcome.refusal,
@@ -408,7 +421,7 @@ export default class LambderInvokeCaller {
408
421
  detail,
409
422
  });
410
423
  }
411
- return await this.failureOutcome(apiName, {
424
+ return await this.#failureOutcome(apiName, {
412
425
  ...shared,
413
426
  ...(outcome.reason === 'refusal' ? { reason: outcome.reason, refusal: outcome.refusal } : { reason: outcome.reason }),
414
427
  response: outcome.response,
@@ -426,7 +439,7 @@ export default class LambderInvokeCaller {
426
439
  // elements read as unknown here; the contract already checked them at
427
440
  // the call site.
428
441
  const [payload, options] = rest;
429
- return await this.dispatch(apiName, payload, options);
442
+ return await this.#dispatch(apiName, payload, options);
430
443
  }
431
444
  /**
432
445
  * The declared output, or a thrown LambderInvokeError carrying the
@@ -437,7 +450,7 @@ export default class LambderInvokeCaller {
437
450
  */
438
451
  async api(apiName, ...rest) {
439
452
  const [payload, options] = rest;
440
- const outcome = await this.dispatch(apiName, payload, options);
453
+ const outcome = await this.#dispatch(apiName, payload, options);
441
454
  if (!outcome.ok)
442
455
  throw outcome.error;
443
456
  return outcome.payload;
@@ -451,28 +464,31 @@ export default class LambderInvokeCaller {
451
464
  async request(init) {
452
465
  const method = (init.method ?? "GET").toUpperCase();
453
466
  const name = `${method} ${init.path}`;
467
+ this.#beforeCall?.(name, { functionName: this.#functionName });
454
468
  const event = synthesizeLambdaHttpEvent({
455
469
  method,
456
470
  path: init.path,
457
471
  query: init.query,
458
- host: this.host,
472
+ host: this.#host,
459
473
  headers: init.headers,
460
474
  clientIp: init.clientIp,
461
475
  cookies: init.cookies,
462
476
  body: init.body,
463
477
  }, { invoke: true });
464
- const delivery = await this.deliverEvent(event, JSON.stringify(event), init);
478
+ const delivery = await this.#deliverEvent(event, JSON.stringify(event), init);
465
479
  if ("failed" in delivery)
466
- throw (await this.failureOutcome(name, delivery.failed)).error;
480
+ throw (await this.#failureOutcome(name, delivery.failed)).error;
467
481
  if (delivery.sent.functionError) {
468
- throw (await this.failureOutcome(name, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) })).error;
482
+ throw (await this.#failureOutcome(name, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) })).error;
469
483
  }
470
484
  try {
471
- return await decodeLambdaHttpResult(delivery.sent.result, this.maxResponsePayloadBytes);
485
+ return await decodeLambdaHttpResult(delivery.sent.result, this.#maxResponsePayloadBytes);
472
486
  }
473
487
  catch (err) {
474
488
  const cause = coerceToError(err, "the answer could not be decoded");
475
- throw (await this.failureOutcome(name, { reason: 'protocol', cause, detail: cause.message })).error;
489
+ throw (await this.#failureOutcome(name, { reason: 'protocol', cause, detail: cause.message })).error;
476
490
  }
477
491
  }
478
492
  }
493
+ const LambderInvokeCaller = LambderInvokeCallerCore;
494
+ export default LambderInvokeCaller;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { APIGatewayProxyEvent, APIGatewayProxyEventV2, Context } from "aws-lambda";
12
12
  import type { LambderHttpEventFormat } from "../core/LambderContext.js";
13
+ import { LAMBDER_PARENT_REQUEST_HEADER } from "../shared/wire/LambderInvokeApiId.js";
13
14
  import type { LambderCompressedBrotliPayload, LambderCompressedGzipPayload } from "../shared/wire/LambderRequestPayload.js";
14
15
  /**
15
16
  * Marks a synthesized request as an invoke, for guards and hooks that want to
@@ -20,6 +21,7 @@ import type { LambderCompressedBrotliPayload, LambderCompressedGzipPayload } fro
20
21
  export declare const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
21
22
  /** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
22
23
  export declare const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
24
+ export { LAMBDER_PARENT_REQUEST_HEADER };
23
25
  /** The value of the marker header; a future incompatible event shape would bump it. */
24
26
  export declare const LAMBDER_INVOKE_PROTOCOL = "1";
25
27
  /** A session carried on a user's behalf: the two values a browser holds. */
@@ -89,7 +91,6 @@ export declare function synthesizeLambdaHttpEvent(request: LambderSynthesizedReq
89
91
  * fields.
90
92
  */
91
93
  export declare const buildEnvelopeJson: (fields: {
92
- apiName: string;
93
94
  version?: string;
94
95
  signature?: string;
95
96
  csrf?: string;
@@ -10,8 +10,9 @@
10
10
  */
11
11
  import { restoreBytes } from "../shared/wire/LambderCompressionCodec.js";
12
12
  import { bytesToBase64 } from "../shared/util/LambderBase64.js";
13
- import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
13
+ import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID, LAMBDER_PARENT_REQUEST_HEADER } from "../shared/wire/LambderInvokeApiId.js";
14
14
  import { buildEnvelopeFields } from "../shared/transport/LambderApiTransport.js";
15
+ import { currentRequestId } from "../shared/util/LambderInvocationScope.js";
15
16
  /**
16
17
  * Marks a synthesized request as an invoke, for guards and hooks that want to
17
18
  * tell. Not an authorization: over HTTP it is a header any client can send.
@@ -21,6 +22,7 @@ import { buildEnvelopeFields } from "../shared/transport/LambderApiTransport.js"
21
22
  export const LAMBDER_INVOKE_HEADER = "x-lambder-invoke";
22
23
  /** The invoking function's name, when the caller runs in Lambda; for the callee's logs. */
23
24
  export const LAMBDER_INVOKED_BY_HEADER = "x-lambder-invoked-by";
25
+ export { LAMBDER_PARENT_REQUEST_HEADER };
24
26
  /** The value of the marker header; a future incompatible event shape would bump it. */
25
27
  export const LAMBDER_INVOKE_PROTOCOL = "1";
26
28
  /**
@@ -47,9 +49,10 @@ export function synthesizeLambdaHttpEvent(request, options) {
47
49
  // what this event is. Forwarding a browser's headers wholesale is an
48
50
  // ordinary gateway-lambda pattern, and without these deletes it would let
49
51
  // a browser-shaped request claim to be an invoke, or an invoke name an
50
- // invoking function that did not send it.
52
+ // invoking function or invocation that did not send it.
51
53
  delete headers[LAMBDER_INVOKE_HEADER];
52
54
  delete headers[LAMBDER_INVOKED_BY_HEADER];
55
+ delete headers[LAMBDER_PARENT_REQUEST_HEADER];
53
56
  if (options.invoke)
54
57
  delete headers[FORWARDED_FOR_HEADER];
55
58
  headers.host = request.host;
@@ -59,6 +62,9 @@ export function synthesizeLambdaHttpEvent(request, options) {
59
62
  const invokedBy = typeof process !== "undefined" ? process.env?.AWS_LAMBDA_FUNCTION_NAME : undefined;
60
63
  if (invokedBy)
61
64
  headers[LAMBDER_INVOKED_BY_HEADER] = invokedBy;
65
+ const parentRequestId = currentRequestId();
66
+ if (parentRequestId)
67
+ headers[LAMBDER_PARENT_REQUEST_HEADER] = parentRequestId;
62
68
  }
63
69
  const isBinary = Buffer.isBuffer(request.body);
64
70
  if (request.contentType)
@@ -158,7 +164,6 @@ export const buildEnvelopeJson = (fields) => {
158
164
  // The field set is buildEnvelopeFields', so a field added to the envelope
159
165
  // reaches this sender too; only the payload is this one's own business.
160
166
  const withoutPayload = JSON.stringify(buildEnvelopeFields({
161
- apiName: fields.apiName,
162
167
  version: fields.version,
163
168
  signature: fields.signature,
164
169
  token: fields.csrf ?? "",
@@ -1,5 +1,6 @@
1
1
  import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
2
2
  import { buildTransportEnvelope, LambderTransportFailure, resolveApiPathTarget } from "../shared/transport/LambderApiTransport.js";
3
+ import { apiCallPath } from "../shared/wire/LambderApiNames.js";
3
4
  import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderRequestPayload.js";
4
5
  import { stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
5
6
  import { LOOPBACK_CLIENT_IP } from "../shared/util/LambderClientIp.js";
@@ -39,7 +40,7 @@ export const lambderHandlerTransport = (handler, options = {}) => {
39
40
  // outside a browser to configure) is a URL, and a URL as the event's
40
41
  // rawPath matches no route: every call would 404 on an app that is
41
42
  // wired correctly.
42
- const target = resolveApiPathTarget(request.apiPath);
43
+ const target = resolveApiPathTarget(apiCallPath(request.apiPath, request.apiName));
43
44
  const host = options.host ?? target.host ?? "localhost";
44
45
  const event = synthesizeLambdaHttpEvent({
45
46
  method: "POST",