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.
- package/CHANGELOG.md +217 -0
- package/README.md +25 -15
- package/dist/api/LambderApiAnswer.d.ts +7 -0
- package/dist/api/LambderApiDeclarations.d.ts +194 -0
- package/dist/api/LambderApiDeclarations.js +52 -0
- package/dist/api/LambderApiDefinition.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +4 -1
- package/dist/api/LambderApiGuards.d.ts +21 -22
- package/dist/api/LambderApiGuards.js +1 -1
- package/dist/api/LambderApiRateLimits.d.ts +19 -19
- package/dist/api/LambderApiRateLimits.js +1 -1
- package/dist/api/LambderApiRefusals.d.ts +22 -0
- package/dist/api/LambderApiRefusals.js +13 -0
- package/dist/api/LambderApiRequest.d.ts +15 -6
- package/dist/api/LambderApiRequest.js +8 -9
- package/dist/build/generateApiFiles.d.ts +60 -0
- package/dist/build/generateApiFiles.js +85 -0
- package/dist/build/generatedTables.d.ts +2 -1
- package/dist/build/generatedTables.js +1 -1
- package/dist/build.d.ts +4 -1
- package/dist/build.js +3 -1
- package/dist/client/LambderCaller.d.ts +36 -23
- package/dist/client/LambderCaller.js +88 -67
- package/dist/client/lambderFetchTransport.js +2 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/core/Lambder.d.ts +232 -92
- package/dist/core/Lambder.js +400 -103
- package/dist/core/LambderCallSummary.d.ts +43 -0
- package/dist/core/LambderCallSummary.js +10 -0
- package/dist/core/LambderContext.d.ts +15 -2
- package/dist/core/LambderContext.js +32 -9
- package/dist/core/LambderCreateOptions.d.ts +78 -64
- package/dist/core/LambderCreateOptions.js +39 -21
- package/dist/core/LambderInvocationScope.d.ts +25 -0
- package/dist/core/LambderInvocationScope.js +21 -0
- package/dist/core/LambderResponse.d.ts +11 -0
- package/dist/core/LambderResponse.js +12 -0
- package/dist/core/LambderResponseBuilder.js +5 -1
- package/dist/index.d.ts +13 -7
- package/dist/index.js +3 -1
- package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
- package/dist/invoke/LambderInvokeCaller.js +110 -94
- package/dist/invoke/LambderLambdaEvent.d.ts +2 -1
- package/dist/invoke/LambderLambdaEvent.js +8 -3
- package/dist/invoke/lambderHandlerTransport.js +2 -1
- package/dist/mock/LambderMockApp.d.ts +39 -33
- package/dist/mock/LambderMockApp.js +66 -55
- package/dist/mock/LambderMockCreateOptions.d.ts +8 -10
- package/dist/mock/LambderMockTypes.d.ts +44 -7
- package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
- package/dist/mock/lambderMockInvokeTransport.js +15 -7
- package/dist/mock/lambderMockMswHandler.js +8 -4
- package/dist/mock.d.ts +2 -2
- package/dist/session/LambderSessionManager.d.ts +46 -3
- package/dist/session/LambderSessionManager.js +62 -11
- package/dist/shared/LambderI18n.js +49 -4
- package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
- package/dist/shared/contracts/LambderRateLimiter.js +2 -2
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/transport/LambderApiTransport.js +2 -3
- package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
- package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
- package/dist/shared/util/LambderInvocationScope.js +5 -0
- package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
- package/dist/shared/util/LambderNamedMaps.js +19 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/shared/wire/LambderApiContract.d.ts +4 -18
- package/dist/shared/wire/LambderApiContract.js +1 -1
- package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
- package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
- package/dist/shared/wire/LambderApiNames.d.ts +55 -0
- package/dist/shared/wire/LambderApiNames.js +81 -0
- package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
- package/dist/shared/wire/LambderCallOutcome.js +47 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
- package/dist/shared/wire/LambderInvokeApiId.js +10 -0
- package/dist/shared/wire/LambderUploadSchemas.js +2 -1
- package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
- package/dist/stores/LambderDdbRateLimiter.js +64 -45
- package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
- package/dist/stores/LambderMemoryRateLimiter.js +20 -15
- package/dist/testing/LambderTestApp.d.ts +18 -5
- package/dist/testing/LambderTestApp.js +21 -5
- package/dist/testing/LambderTestVisitor.d.ts +16 -8
- package/dist/testing/LambderTestVisitor.js +33 -25
- 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
|
-
|
|
47
|
-
functionName;
|
|
48
|
-
apiPath;
|
|
49
|
-
apiVersion;
|
|
50
|
-
apiSignatures;
|
|
51
|
-
host;
|
|
52
|
-
requestCompression;
|
|
53
|
-
maxResponsePayloadBytes;
|
|
54
|
-
timeoutMs;
|
|
55
|
-
onLogList;
|
|
56
|
-
onFailure;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
68
|
-
this
|
|
69
|
-
this
|
|
70
|
-
this
|
|
71
|
-
this
|
|
72
|
-
this
|
|
73
|
-
this
|
|
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
|
|
76
|
-
this
|
|
77
|
-
this
|
|
78
|
-
this
|
|
79
|
-
this
|
|
80
|
-
this
|
|
81
|
-
this
|
|
82
|
-
this
|
|
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
|
|
145
|
-
this
|
|
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
|
|
161
|
+
return this.#sdk;
|
|
150
162
|
}
|
|
151
|
-
async invokeThroughSdk(eventJson, signal) {
|
|
152
|
-
const { LambdaClient, InvokeCommand } = await this
|
|
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
|
|
157
|
-
this
|
|
158
|
-
const output = await this
|
|
159
|
-
FunctionName: this
|
|
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
|
|
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
|
|
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
|
|
237
|
+
message: `${this.#functionName} ${apiName} failed (${init.reason}): ${detail}`,
|
|
226
238
|
reason: init.reason,
|
|
227
239
|
apiName,
|
|
228
|
-
functionName: this
|
|
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
|
|
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
|
|
274
|
+
await this.#onFailure(failure, { apiName, functionName: this.#functionName });
|
|
263
275
|
}
|
|
264
276
|
catch (err) {
|
|
265
|
-
console.error(`[lambder invoke] onFailure threw for ${this
|
|
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
|
|
285
|
+
if (this.#onLogList) {
|
|
274
286
|
try {
|
|
275
|
-
await this
|
|
287
|
+
await this.#onLogList(apiName, logList);
|
|
276
288
|
}
|
|
277
289
|
catch (err) {
|
|
278
|
-
console.error(`[lambder invoke] onLogList threw for ${this
|
|
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
|
|
295
|
+
console.log(`[lambder invoke] ${this.#functionName} ${apiName}`, entry);
|
|
284
296
|
}
|
|
285
297
|
/**
|
|
286
|
-
* One call, one outcome. Never throws
|
|
287
|
-
* is told how the attempt ended, as it
|
|
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
|
|
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
|
|
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
|
|
316
|
-
? await this
|
|
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
|
|
338
|
+
const compressionMinBytes = resolveRequestCompressionMinBytes(options.compressRequest, this.#requestCompression);
|
|
325
339
|
const compressed = compressionMinBytes !== null && payloadJson !== undefined
|
|
326
|
-
? await compressPayloadBrotli(payloadJson, compressionMinBytes, this
|
|
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
|
|
331
|
-
host: this
|
|
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
|
|
349
|
+
cookies: sessionCookies(options.session, this.#sessionTokenCookieKey),
|
|
336
350
|
body: buildEnvelopeJson({
|
|
337
|
-
|
|
338
|
-
version: this.apiVersion,
|
|
351
|
+
version: this.#apiVersion,
|
|
339
352
|
signature,
|
|
340
353
|
csrf: options.session?.csrf,
|
|
341
|
-
siteHost: this
|
|
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
|
|
368
|
+
return await this.#failureOutcome(apiName, { reason: 'unknown', cause: coerceToError(err, "the call could not be built") });
|
|
356
369
|
}
|
|
357
|
-
const delivery = await this
|
|
370
|
+
const delivery = await this.#deliverEvent(event, eventJson, options);
|
|
358
371
|
if ("failed" in delivery)
|
|
359
|
-
return await this
|
|
372
|
+
return await this.#failureOutcome(apiName, delivery.failed);
|
|
360
373
|
if (delivery.sent.functionError) {
|
|
361
|
-
return await this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
412
|
+
? `no API at ${this.#apiPath} on ${this.#functionName} (HTTP 404): does apiPath match the callee's?`
|
|
400
413
|
: undefined;
|
|
401
|
-
return await this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
478
|
+
const delivery = await this.#deliverEvent(event, JSON.stringify(event), init);
|
|
465
479
|
if ("failed" in delivery)
|
|
466
|
-
throw (await this
|
|
480
|
+
throw (await this.#failureOutcome(name, delivery.failed)).error;
|
|
467
481
|
if (delivery.sent.functionError) {
|
|
468
|
-
throw (await this
|
|
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
|
|
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
|
|
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",
|