lambder 11.0.1 → 12.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 +154 -0
- package/README.md +18 -13
- 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/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/LambderApiRequest.d.ts +15 -6
- package/dist/api/LambderApiRequest.js +8 -9
- package/dist/build/generatedTables.d.ts +2 -1
- package/dist/build/generatedTables.js +1 -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 +203 -87
- package/dist/core/Lambder.js +265 -86
- package/dist/core/LambderContext.d.ts +1 -1
- package/dist/core/LambderContext.js +13 -5
- package/dist/core/LambderCreateOptions.d.ts +28 -39
- package/dist/core/LambderCreateOptions.js +2 -3
- package/dist/core/LambderPublicFiles.d.ts +8 -1
- package/dist/core/LambderPublicFiles.js +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -0
- package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
- package/dist/invoke/LambderInvokeCaller.js +110 -94
- package/dist/invoke/LambderLambdaEvent.d.ts +0 -1
- package/dist/invoke/LambderLambdaEvent.js +0 -1
- package/dist/invoke/lambderHandlerTransport.js +2 -1
- package/dist/mock/LambderMockApp.d.ts +31 -25
- package/dist/mock/LambderMockApp.js +52 -45
- package/dist/mock/LambderMockCreateOptions.d.ts +6 -7
- 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/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/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/LambderUploadSchemas.js +2 -1
- package/dist/testing/LambderTestApp.d.ts +5 -1
- 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;
|
|
@@ -158,7 +158,6 @@ export const buildEnvelopeJson = (fields) => {
|
|
|
158
158
|
// The field set is buildEnvelopeFields', so a field added to the envelope
|
|
159
159
|
// reaches this sender too; only the payload is this one's own business.
|
|
160
160
|
const withoutPayload = JSON.stringify(buildEnvelopeFields({
|
|
161
|
-
apiName: fields.apiName,
|
|
162
161
|
version: fields.version,
|
|
163
162
|
signature: fields.signature,
|
|
164
163
|
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",
|
|
@@ -13,7 +13,7 @@ import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.j
|
|
|
13
13
|
import LambderSessionManager, { type LambderCreatedSession } from "../session/LambderSessionManager.js";
|
|
14
14
|
import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./LambderMockCreateOptions.js";
|
|
15
15
|
import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
|
|
16
|
-
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener,
|
|
16
|
+
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockNotMockedInput, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSlice, LambderMockOverride, LambderMockRefusalsOf } from "./LambderMockTypes.js";
|
|
17
17
|
/**
|
|
18
18
|
* The mock runtime: the API core (LambderApiPipeline, the same class the
|
|
19
19
|
* Lambda server runs) over memory stores, with typed mock handlers and mock
|
|
@@ -69,6 +69,8 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
69
69
|
private readonly registry;
|
|
70
70
|
/** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
|
|
71
71
|
private readonly apiOptions;
|
|
72
|
+
/** The mock's own guards as create() was given them: which of them need a session, for an entry's mode without the apiOptions table. */
|
|
73
|
+
private readonly mockGuards;
|
|
72
74
|
/** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
|
|
73
75
|
private readonly guardDeclarations;
|
|
74
76
|
/** The server's refusal vocabulary, when create() was given the generated table: whether each code an entry may refuse with carries data. */
|
|
@@ -93,12 +95,19 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
93
95
|
private assertEntryRegistration;
|
|
94
96
|
/**
|
|
95
97
|
* The table's entry for an endpoint, when create() was given one: null
|
|
96
|
-
* without a table, and a throw for a name the table does not hold
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* the compiler did not see.
|
|
98
|
+
* without a table, and a throw for a name the table does not hold. The
|
|
99
|
+
* compiler already refuses it against the contract; this is where a
|
|
100
|
+
* stale table meets a caller the compiler did not see.
|
|
100
101
|
*/
|
|
101
102
|
private declaredOptionsOf;
|
|
103
|
+
/**
|
|
104
|
+
* An entry's mode, by the server's rule: a session endpoint when one of
|
|
105
|
+
* its guards needs a session. The guards are the apiOptions table's when
|
|
106
|
+
* create() was given one (which records the mode it came to as well), and
|
|
107
|
+
* otherwise the ones the entry restates, each read off the guardDeclarations
|
|
108
|
+
* table or the mock's own guards.
|
|
109
|
+
*/
|
|
110
|
+
private modeOf;
|
|
102
111
|
/**
|
|
103
112
|
* The codes an entry may refuse with, read off the generated tables (its
|
|
104
113
|
* own from apiOptions, its guards' from guardDeclarations), each resolved
|
|
@@ -112,25 +121,23 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
112
121
|
/** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
|
|
113
122
|
private declaredModeOf;
|
|
114
123
|
private buildEntry;
|
|
115
|
-
/** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
|
|
116
|
-
publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
117
|
-
/** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
|
|
118
|
-
sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
119
124
|
/**
|
|
120
|
-
* A
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* so the
|
|
125
|
+
* A mock for an endpoint: a handler, or the handler with the endpoint's
|
|
126
|
+
* declarations restated. Its mode is the server's (see modeOf): a
|
|
127
|
+
* session endpoint's session is fetched before the handler runs, and a
|
|
128
|
+
* call without one is refused as on the server.
|
|
129
|
+
*/
|
|
130
|
+
api<K extends keyof C & string, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
131
|
+
/**
|
|
132
|
+
* An endpoint deliberately left without a mock; a call answers the
|
|
133
|
+
* notMocked refusal carrying the reason. The refusal runs through the
|
|
134
|
+
* pipeline, so the steps before dispatch still happen, the session read
|
|
135
|
+
* among them, under the server's mode (see modeOf): a signed-out call to
|
|
136
|
+
* a session endpoint answers sessionExpired as on the server. Without the
|
|
137
|
+
* apiOptions table a session endpoint restates its guards to say so,
|
|
138
|
+
* `notMocked(name, { reason, guards })`; no guard runs.
|
|
130
139
|
*/
|
|
131
|
-
notMocked<K extends
|
|
132
|
-
/** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
|
|
133
|
-
sessionNotMocked<K extends LambderMockSessionNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
|
|
140
|
+
notMocked<K extends keyof C & string>(name: K, input: LambderMockNotMockedInput<C, K, TDerived>): LambderMockEntry<C, K>;
|
|
134
141
|
/**
|
|
135
142
|
* "Everything I did not register is not mocked, for this reason", as an
|
|
136
143
|
* argument to the same register() call:
|
|
@@ -154,11 +161,10 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
154
161
|
* a type), and a call it answers is processed as public: the protocol's
|
|
155
162
|
* pre-pass still runs, so a stale client still hears versionExpired, but
|
|
156
163
|
* a signed-out call to an unmocked session endpoint answers "not mocked"
|
|
157
|
-
* where the server answers sessionExpired.
|
|
158
|
-
*
|
|
164
|
+
* where the server answers sessionExpired. Give create() the table where
|
|
165
|
+
* a test cares about that path.
|
|
159
166
|
*/
|
|
160
167
|
restNotMocked(reason: string): LambderMockRestEntry;
|
|
161
|
-
private buildNotMockedEntry;
|
|
162
168
|
/** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
|
|
163
169
|
apiSlice<const E extends readonly LambderMockEntry<C, keyof C & string>[]>(...entries: E): LambderMockSlice<C, E[number]["name"]>;
|
|
164
170
|
private addSlices;
|