@stone-js/aws-lambda-http-adapter 0.3.1 → 0.8.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/README.md +10 -10
- package/dist/AWSLambdaHttpAdapter.d.ts +85 -0
- package/dist/AwsLambdaHttpErrorHandler.d.ts +28 -0
- package/dist/RawHttpResponseWrapper.d.ts +69 -0
- package/dist/browser/decorators/AwsLambdaHttp.d.ts +38 -0
- package/dist/browser/options/NodeHttpAdapterBlueprint.d.ts +13 -0
- package/dist/browser.js +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/declarations.d.ts +124 -0
- package/dist/decorators/AwsLambdaHttp.d.ts +34 -0
- package/dist/errors/AwsLambdaHttpAdapterError.d.ts +7 -0
- package/dist/event-normalizer.d.ts +110 -0
- package/dist/index.d.ts +15 -668
- package/dist/index.js +473 -414
- package/dist/middleware/BlueprintMiddleware.d.ts +21 -0
- package/dist/middleware/BodyEventMiddleware.d.ts +78 -0
- package/dist/middleware/FilesEventMiddleware.d.ts +42 -0
- package/dist/middleware/IncomingEventMiddleware.d.ts +97 -0
- package/dist/middleware/ServerResponseMiddleware.d.ts +23 -0
- package/dist/options/AwsLambdaHttpAdapterBlueprint.d.ts +41 -0
- package/dist/resolvers.d.ts +10 -0
- package/package.json +25 -25
package/dist/index.js
CHANGED
|
@@ -1,47 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IntegrationError, Adapter, AdapterEventBuilder, defaultLoggerResolver, defaultKernelResolver, isNotEmpty, classDecoratorLegacyWrapper, addBlueprint } from '@stone-js/core';
|
|
2
|
+
import { IncomingHttpEvent, HTTP_INTERNAL_SERVER_ERROR, BinaryFileResponse, OutgoingHttpResponse, CookieCollection, isIpTrusted, getHostname, getProtocol, httpCoreBlueprint, isMultipart, getCharset, getFilesUploads } from '@stone-js/http-core';
|
|
2
3
|
import mime from 'mime';
|
|
3
4
|
import accepts from 'accepts';
|
|
4
5
|
import statuses from 'statuses';
|
|
5
|
-
import {
|
|
6
|
+
import { cloneValue, deepMerge } from '@stone-js/config';
|
|
7
|
+
import { getString } from '@stone-js/env';
|
|
6
8
|
import { File } from '@stone-js/filesystem';
|
|
7
9
|
import proxyAddr from 'proxy-addr';
|
|
8
10
|
import bytes from 'bytes';
|
|
9
11
|
import typeIs from 'type-is';
|
|
10
|
-
import { getString } from '@stone-js/env';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Class representing an AwsLambdaHttpErrorHandler.
|
|
14
|
-
*/
|
|
15
|
-
class AwsLambdaHttpErrorHandler {
|
|
16
|
-
logger;
|
|
17
|
-
/**
|
|
18
|
-
* Create an NodeHttpErrorHandler.
|
|
19
|
-
*
|
|
20
|
-
* @param options - NodeHttpErrorHandler options.
|
|
21
|
-
*/
|
|
22
|
-
constructor({ blueprint }) {
|
|
23
|
-
this.logger = blueprint.get('stone.logger.resolver', defaultLoggerResolver)(blueprint);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Handle an error.
|
|
27
|
-
*
|
|
28
|
-
* @param error - The error to handle.
|
|
29
|
-
* @param context - The context of the adapter.
|
|
30
|
-
* @returns The raw response builder.
|
|
31
|
-
*/
|
|
32
|
-
handle(error, context) {
|
|
33
|
-
this.logger.error(error.message, { error });
|
|
34
|
-
const statusCode = error.cause?.status ?? HTTP_INTERNAL_SERVER_ERROR;
|
|
35
|
-
const type = accepts(context.rawEvent).type(['json', 'html']);
|
|
36
|
-
const contentType = mime.getType(type !== false ? type : 'txt') ?? context.rawEvent.headers['content-type'] ?? 'text/plain';
|
|
37
|
-
const headers = new Headers({ 'Content-Type': contentType });
|
|
38
|
-
return context
|
|
39
|
-
.rawResponseBuilder
|
|
40
|
-
.add('headers', headers)
|
|
41
|
-
.add('statusCode', statusCode)
|
|
42
|
-
.add('statusMessage', statuses.message[statusCode]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
12
|
|
|
46
13
|
/**
|
|
47
14
|
* Wrapper for HTTP raw responses in AWS Lambda.
|
|
@@ -105,23 +72,40 @@ class RawHttpResponseWrapper {
|
|
|
105
72
|
* ```
|
|
106
73
|
*/
|
|
107
74
|
respond() {
|
|
108
|
-
|
|
75
|
+
const headers = new Headers(this.options.headers ?? {});
|
|
76
|
+
const setCookies = this.extractSetCookies(headers);
|
|
77
|
+
headers.delete('set-cookie');
|
|
78
|
+
const response = {
|
|
109
79
|
...this.options,
|
|
110
80
|
statusCode: this.options.statusCode ?? 500,
|
|
111
|
-
headers:
|
|
81
|
+
headers: Object.fromEntries(headers)
|
|
112
82
|
};
|
|
83
|
+
// Emit multiple Set-Cookie correctly per trigger: v2/Function URLs use the `cookies` field,
|
|
84
|
+
// v1/ALB use `multiValueHeaders`. Folding them into a single comma-joined header (the old
|
|
85
|
+
// behaviour) is unparseable by browsers and leaks a cookie's attributes into the next.
|
|
86
|
+
if (setCookies.length > 0) {
|
|
87
|
+
if (this.options.version === 'v2') {
|
|
88
|
+
response.cookies = setCookies;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
response.multiValueHeaders = { ...this.options.multiValueHeaders, 'set-cookie': setCookies };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
113
95
|
}
|
|
114
96
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* Converts Headers or Record<string, string> to a normalized Record<string, string>
|
|
118
|
-
* with all keys in lowercase.
|
|
97
|
+
* Extract all `Set-Cookie` values from a Headers instance, tolerant of runtimes without
|
|
98
|
+
* `getSetCookie()`.
|
|
119
99
|
*
|
|
120
|
-
* @param headers - The headers
|
|
121
|
-
* @returns
|
|
100
|
+
* @param headers - The response headers.
|
|
101
|
+
* @returns The raw `Set-Cookie` strings.
|
|
122
102
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
extractSetCookies(headers) {
|
|
104
|
+
if (typeof headers.getSetCookie === 'function') {
|
|
105
|
+
return headers.getSetCookie();
|
|
106
|
+
}
|
|
107
|
+
const raw = headers.get('set-cookie');
|
|
108
|
+
return raw !== null && raw.length > 0 ? [raw] : [];
|
|
125
109
|
}
|
|
126
110
|
}
|
|
127
111
|
|
|
@@ -214,7 +198,7 @@ class AwsLambdaHttpAdapter extends Adapter {
|
|
|
214
198
|
*/
|
|
215
199
|
async onStart() {
|
|
216
200
|
if (typeof window === 'object') {
|
|
217
|
-
throw new AwsLambdaHttpAdapterError('This `
|
|
201
|
+
throw new AwsLambdaHttpAdapterError('This `AwsLambdaHttpAdapter` must be used only in AWS Lambda context.');
|
|
218
202
|
}
|
|
219
203
|
await this.executeHooks('onStart');
|
|
220
204
|
}
|
|
@@ -255,6 +239,46 @@ class AwsLambdaHttpAdapter extends Adapter {
|
|
|
255
239
|
}
|
|
256
240
|
}
|
|
257
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Class representing an AwsLambdaHttpErrorHandler.
|
|
244
|
+
*/
|
|
245
|
+
class AwsLambdaHttpErrorHandler {
|
|
246
|
+
logger;
|
|
247
|
+
/**
|
|
248
|
+
* Create an NodeHttpErrorHandler.
|
|
249
|
+
*
|
|
250
|
+
* @param options - NodeHttpErrorHandler options.
|
|
251
|
+
*/
|
|
252
|
+
constructor({ blueprint }) {
|
|
253
|
+
this.logger = blueprint.get('stone.logger.resolver', defaultLoggerResolver)(blueprint);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Handle an error.
|
|
257
|
+
*
|
|
258
|
+
* @param error - The error to handle.
|
|
259
|
+
* @param context - The context of the adapter.
|
|
260
|
+
* @returns The raw response builder.
|
|
261
|
+
*/
|
|
262
|
+
handle(error, context) {
|
|
263
|
+
this.logger.error(error.message, { error });
|
|
264
|
+
// http-core's HttpError carries `statusCode` on the error itself; fall back to a cause and
|
|
265
|
+
// finally to 500. (Reading only `cause.status` turned every typed HTTP error into a 500.)
|
|
266
|
+
const statusCode = error.statusCode ??
|
|
267
|
+
error.cause?.statusCode ??
|
|
268
|
+
error.cause?.status ??
|
|
269
|
+
HTTP_INTERNAL_SERVER_ERROR;
|
|
270
|
+
const requestHeaders = context.rawEvent.headers ?? {};
|
|
271
|
+
const type = accepts({ headers: requestHeaders }).type(['json', 'html']);
|
|
272
|
+
const contentType = mime.getType(type !== false ? type : 'txt') ?? requestHeaders['content-type'] ?? 'text/plain';
|
|
273
|
+
const headers = new Headers({ 'Content-Type': contentType });
|
|
274
|
+
return context
|
|
275
|
+
.rawResponseBuilder
|
|
276
|
+
.add('headers', headers)
|
|
277
|
+
.add('statusCode', statusCode)
|
|
278
|
+
.add('statusMessage', statuses.message[statusCode]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
258
282
|
/**
|
|
259
283
|
* A constant representing the AWS Lambda HTTP platform identifier.
|
|
260
284
|
*
|
|
@@ -264,6 +288,171 @@ class AwsLambdaHttpAdapter extends Adapter {
|
|
|
264
288
|
*/
|
|
265
289
|
const AWS_LAMBDA_HTTP_PLATFORM = 'aws_lambda_http';
|
|
266
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Detect which AWS HTTP trigger produced the event.
|
|
293
|
+
*
|
|
294
|
+
* @param event - The raw Lambda event.
|
|
295
|
+
* @returns The trigger family.
|
|
296
|
+
*/
|
|
297
|
+
function detectEventVersion(event) {
|
|
298
|
+
const requestContext = event.requestContext;
|
|
299
|
+
if (requestContext?.elb !== undefined) {
|
|
300
|
+
return 'alb';
|
|
301
|
+
}
|
|
302
|
+
if (event.version === '2.0' || requestContext?.http !== undefined) {
|
|
303
|
+
return 'v2';
|
|
304
|
+
}
|
|
305
|
+
return 'v1';
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Normalize headers to lower-cased keys, merging single and multi-value headers.
|
|
309
|
+
*
|
|
310
|
+
* @param event - The raw Lambda event.
|
|
311
|
+
* @returns Lower-cased headers with multi-value entries joined by `, `.
|
|
312
|
+
*/
|
|
313
|
+
function normalizeHeaders(event) {
|
|
314
|
+
const out = {};
|
|
315
|
+
const multi = event.multiValueHeaders;
|
|
316
|
+
if (multi !== null && multi !== undefined) {
|
|
317
|
+
for (const [name, values] of Object.entries(multi)) {
|
|
318
|
+
if (Array.isArray(values)) {
|
|
319
|
+
out[name.toLowerCase()] = values.join(', ');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const single = event.headers;
|
|
324
|
+
if (single !== null && single !== undefined) {
|
|
325
|
+
for (const [name, value] of Object.entries(single)) {
|
|
326
|
+
if (value !== undefined) {
|
|
327
|
+
out[name.toLowerCase()] = value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Build the raw query string from whichever representation the trigger provides.
|
|
335
|
+
*
|
|
336
|
+
* v2 gives the fidelity-preserving `rawQueryString`. v1/ALB give an object (single value) and
|
|
337
|
+
* optionally `multiValueQueryStringParameters` (preferred, preserves repeated keys). Values are
|
|
338
|
+
* URL-encoded so repeated/array values survive.
|
|
339
|
+
*
|
|
340
|
+
* @param event - The raw Lambda event.
|
|
341
|
+
* @param version - The detected trigger family.
|
|
342
|
+
* @returns The raw query string (no leading `?`).
|
|
343
|
+
*/
|
|
344
|
+
function buildRawQueryString(event, version) {
|
|
345
|
+
if (version === 'v2' && typeof event.rawQueryString === 'string') {
|
|
346
|
+
return event.rawQueryString;
|
|
347
|
+
}
|
|
348
|
+
const multi = event.multiValueQueryStringParameters;
|
|
349
|
+
if (multi !== null && multi !== undefined) {
|
|
350
|
+
const params = new URLSearchParams();
|
|
351
|
+
for (const [key, values] of Object.entries(multi)) {
|
|
352
|
+
for (const value of values ?? []) {
|
|
353
|
+
params.append(key, value);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return params.toString();
|
|
357
|
+
}
|
|
358
|
+
const single = event.queryStringParameters;
|
|
359
|
+
if (single !== null && single !== undefined) {
|
|
360
|
+
const params = new URLSearchParams();
|
|
361
|
+
for (const [key, value] of Object.entries(single)) {
|
|
362
|
+
if (value !== undefined) {
|
|
363
|
+
params.append(key, value);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return params.toString();
|
|
367
|
+
}
|
|
368
|
+
return '';
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Collect the raw cookie strings, regardless of trigger.
|
|
372
|
+
*
|
|
373
|
+
* v2 delivers `event.cookies: string[]`. v1/ALB deliver a single `Cookie` header (already merged
|
|
374
|
+
* into {@link NormalizedHttpEvent.headers}).
|
|
375
|
+
*
|
|
376
|
+
* @param event - The raw Lambda event.
|
|
377
|
+
* @param headers - The normalized headers.
|
|
378
|
+
* @returns The raw cookie strings.
|
|
379
|
+
*/
|
|
380
|
+
function collectCookies(event, headers) {
|
|
381
|
+
if (Array.isArray(event.cookies)) {
|
|
382
|
+
return event.cookies;
|
|
383
|
+
}
|
|
384
|
+
const header = headers.cookie;
|
|
385
|
+
return typeof header === 'string' && header.length > 0 ? header.split(/; */) : [];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Resolve the client source IP across triggers.
|
|
389
|
+
*
|
|
390
|
+
* @param event - The raw Lambda event.
|
|
391
|
+
* @param version - The detected trigger family.
|
|
392
|
+
* @param headers - The normalized headers.
|
|
393
|
+
* @returns The source IP (empty string if unknown).
|
|
394
|
+
*/
|
|
395
|
+
function resolveSourceIp(event, version, headers) {
|
|
396
|
+
const requestContext = event.requestContext;
|
|
397
|
+
if (version === 'v2') {
|
|
398
|
+
return String(requestContext?.http?.sourceIp ?? '');
|
|
399
|
+
}
|
|
400
|
+
if (version === 'v1') {
|
|
401
|
+
return String(requestContext?.identity?.sourceIp ?? '');
|
|
402
|
+
}
|
|
403
|
+
// ALB carries no identity; the client IP is the first hop in X-Forwarded-For.
|
|
404
|
+
return (headers['x-forwarded-for'] ?? '').split(',')[0].trim();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Normalize any AWS HTTP Lambda event into the canonical {@link NormalizedHttpEvent}.
|
|
408
|
+
*
|
|
409
|
+
* @param event - The raw Lambda event.
|
|
410
|
+
* @returns The normalized request.
|
|
411
|
+
*/
|
|
412
|
+
function normalizeHttpEvent(event) {
|
|
413
|
+
const version = detectEventVersion(event);
|
|
414
|
+
const headers = normalizeHeaders(event);
|
|
415
|
+
const requestContext = event.requestContext;
|
|
416
|
+
const method = String((version === 'v2' ? requestContext?.http?.method : undefined) ??
|
|
417
|
+
event.httpMethod ??
|
|
418
|
+
requestContext?.httpMethod ??
|
|
419
|
+
requestContext?.http?.method ??
|
|
420
|
+
'GET').toUpperCase();
|
|
421
|
+
const path = String((version === 'v2' ? (event.rawPath ?? requestContext?.http?.path) : undefined) ??
|
|
422
|
+
event.path ??
|
|
423
|
+
event.rawPath ??
|
|
424
|
+
'/');
|
|
425
|
+
return {
|
|
426
|
+
version,
|
|
427
|
+
method,
|
|
428
|
+
path,
|
|
429
|
+
headers,
|
|
430
|
+
rawQueryString: buildRawQueryString(event, version),
|
|
431
|
+
cookies: collectCookies(event, headers),
|
|
432
|
+
sourceIp: resolveSourceIp(event, version, headers),
|
|
433
|
+
isBase64Encoded: event.isBase64Encoded === true,
|
|
434
|
+
body: typeof event.body === 'string' ? event.body : undefined
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Extract the raw request body exactly as received, decoding base64 to a Buffer when needed.
|
|
439
|
+
*
|
|
440
|
+
* This is what the adapter exposes as `metadata.rawBody`: the untouched payload the client sent,
|
|
441
|
+
* available to consumers even when no body-parsing middleware is installed. Binary payloads are
|
|
442
|
+
* returned as a `Buffer` (never a lossy UTF-8 round-trip); text payloads as a string. A
|
|
443
|
+
* non-string, non-base64 body (e.g. a pre-parsed object from a test/custom integration) is
|
|
444
|
+
* returned as-is.
|
|
445
|
+
*
|
|
446
|
+
* @param event - The raw Lambda event.
|
|
447
|
+
* @returns The raw body as a Buffer, string, the original value, or undefined.
|
|
448
|
+
*/
|
|
449
|
+
function getRawBody(event) {
|
|
450
|
+
if (typeof event.body === 'string') {
|
|
451
|
+
return event.isBase64Encoded === true ? Buffer.from(event.body, 'base64') : event.body;
|
|
452
|
+
}
|
|
453
|
+
return event.body;
|
|
454
|
+
}
|
|
455
|
+
|
|
267
456
|
/**
|
|
268
457
|
* Adapter resolver for AWS Lambda HTTP adapter.
|
|
269
458
|
*
|
|
@@ -308,83 +497,14 @@ const metaAdapterBlueprintMiddleware = [
|
|
|
308
497
|
{ module: SetAwsLambdaHttpResponseResolverMiddleware, priority: 6 }
|
|
309
498
|
];
|
|
310
499
|
|
|
311
|
-
/**
|
|
312
|
-
* Class representing a FilesEventMiddleware.
|
|
313
|
-
*
|
|
314
|
-
* @author Mr. Stone <evensstone@gmail.com>
|
|
315
|
-
*/
|
|
316
|
-
class FilesEventMiddleware {
|
|
317
|
-
/**
|
|
318
|
-
* The blueprint for resolving configuration and dependencies.
|
|
319
|
-
*/
|
|
320
|
-
blueprint;
|
|
321
|
-
/**
|
|
322
|
-
* Create a FilesEventMiddleware.
|
|
323
|
-
*
|
|
324
|
-
* @param {blueprint} options - Options for creating the FilesEventMiddleware.
|
|
325
|
-
*/
|
|
326
|
-
constructor({ blueprint }) {
|
|
327
|
-
this.blueprint = blueprint;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Handles the incoming event, processes it, and invokes the next middleware in the pipeline.
|
|
331
|
-
*
|
|
332
|
-
* @param context - The adapter context containing the raw event, execution context, and other data.
|
|
333
|
-
* @param next - The next middleware to be invoked in the pipeline.
|
|
334
|
-
* @returns A promise that resolves to the destination type after processing.
|
|
335
|
-
*
|
|
336
|
-
* @throws {AwsLambdaHttpAdapterError} If required components such as the rawEvent or IncomingEventBuilder are not provided.
|
|
337
|
-
*/
|
|
338
|
-
async handle(context, next) {
|
|
339
|
-
if (context.rawEvent === undefined || context.incomingEventBuilder?.add === undefined) {
|
|
340
|
-
throw new AwsLambdaHttpAdapterError('The context is missing required components.');
|
|
341
|
-
}
|
|
342
|
-
if (isMultipart(this.normalizeEvent(context.rawEvent))) {
|
|
343
|
-
const options = this.blueprint.get('stone.http.files.upload', {});
|
|
344
|
-
const response = await getFilesUploads(this.normalizeEvent(context.rawEvent), options);
|
|
345
|
-
const method = response.fields.$method$;
|
|
346
|
-
context
|
|
347
|
-
.incomingEventBuilder
|
|
348
|
-
.add('files', response.files)
|
|
349
|
-
.add('body', response.fields);
|
|
350
|
-
// In fullstack forms, the method is spoofed and sent as a hidden field
|
|
351
|
-
isNotEmpty(method) && context.incomingEventBuilder.add('method', method);
|
|
352
|
-
}
|
|
353
|
-
return await next(context);
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Normalize the incoming event to an IncomingMessage.
|
|
357
|
-
*
|
|
358
|
-
* @param rawEvent - The raw event to be normalized.
|
|
359
|
-
* @returns The normalized event.
|
|
360
|
-
*/
|
|
361
|
-
normalizeEvent(rawEvent) {
|
|
362
|
-
let body = rawEvent.body;
|
|
363
|
-
if (typeof rawEvent.body === 'string') {
|
|
364
|
-
body = rawEvent.isBase64Encoded === true
|
|
365
|
-
? Buffer.from(rawEvent.body, 'base64')
|
|
366
|
-
: Buffer.from(rawEvent.body, 'utf-8');
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
body,
|
|
370
|
-
headers: {
|
|
371
|
-
'content-type': rawEvent.headers['content-type'] ?? rawEvent.headers['Content-Type'],
|
|
372
|
-
'content-length': rawEvent.headers['content-length'] ?? rawEvent.headers['Content-Length'],
|
|
373
|
-
'transfer-encoding': rawEvent.headers['transfer-encoding'] ?? rawEvent.headers['Transfer-Encoding']
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Meta Middleware for processing files uploads.
|
|
380
|
-
*/
|
|
381
|
-
const MetaFilesEventMiddleware = { module: FilesEventMiddleware, isClass: true };
|
|
382
|
-
|
|
383
500
|
/**
|
|
384
501
|
* Middleware for handling incoming events and transforming them into Stone.js events.
|
|
385
502
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
503
|
+
* It first normalizes the raw AWS event (API Gateway v1/v2, ALB, Function URLs) into a single
|
|
504
|
+
* canonical shape, then extracts URL, IP addresses, headers, cookies, query and the raw body,
|
|
505
|
+
* so the pipeline never has to reason about which trigger fired. The untouched request body is
|
|
506
|
+
* always exposed as `metadata.rawBody` — even when no body-parsing middleware is installed — so
|
|
507
|
+
* consumers can read the original payload (e.g. to verify a webhook signature).
|
|
388
508
|
*/
|
|
389
509
|
class IncomingEventMiddleware {
|
|
390
510
|
/**
|
|
@@ -394,7 +514,7 @@ class IncomingEventMiddleware {
|
|
|
394
514
|
/**
|
|
395
515
|
* Create an IncomingEventMiddleware instance.
|
|
396
516
|
*
|
|
397
|
-
* @param
|
|
517
|
+
* @param options - Options containing the blueprint for resolving configuration and dependencies.
|
|
398
518
|
*/
|
|
399
519
|
constructor({ blueprint }) {
|
|
400
520
|
this.blueprint = blueprint;
|
|
@@ -411,23 +531,23 @@ class IncomingEventMiddleware {
|
|
|
411
531
|
if ((context.rawEvent === undefined) || ((context.incomingEventBuilder?.add) === undefined)) {
|
|
412
532
|
throw new AwsLambdaHttpAdapterError('The context is missing required components.');
|
|
413
533
|
}
|
|
534
|
+
const event = normalizeHttpEvent(context.rawEvent);
|
|
414
535
|
const proxyOptions = this.getProxyOptions();
|
|
415
536
|
const cookieOptions = this.getCookieOptions();
|
|
416
|
-
const url = this.extractUrl(context.rawEvent, proxyOptions);
|
|
417
|
-
const ipAddresses = this.extractIpAddresses(context.rawEvent, proxyOptions);
|
|
418
537
|
context
|
|
419
538
|
.incomingEventBuilder
|
|
420
|
-
.add('url',
|
|
421
|
-
.add('ips',
|
|
539
|
+
.add('url', this.extractUrl(event, proxyOptions))
|
|
540
|
+
.add('ips', this.extractIpAddresses(event, proxyOptions))
|
|
422
541
|
.add('source', this.getSource(context))
|
|
423
|
-
.add('headers',
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.
|
|
428
|
-
.add('
|
|
429
|
-
.add('
|
|
430
|
-
.add('
|
|
542
|
+
.add('headers', event.headers)
|
|
543
|
+
// Expose the untouched request body so any consumer can read it, regardless of parsing.
|
|
544
|
+
.add('metadata', { rawBody: getRawBody(context.rawEvent) })
|
|
545
|
+
// If not defined by other middleware; in fullstack forms the method is spoofed via a field.
|
|
546
|
+
.addIf('method', event.method)
|
|
547
|
+
.add('queryString', event.rawQueryString)
|
|
548
|
+
.add('protocol', this.getProtocol(event, proxyOptions))
|
|
549
|
+
.add('cookies', CookieCollection.create(event.cookies.join('; '), cookieOptions, this.getCookieSecret()))
|
|
550
|
+
.add('ip', proxyAddr(this.toNodeMessage(event), isIpTrusted(proxyOptions.trustedIp, proxyOptions.untrustedIp)));
|
|
431
551
|
return await next(context);
|
|
432
552
|
}
|
|
433
553
|
/**
|
|
@@ -443,18 +563,6 @@ class IncomingEventMiddleware {
|
|
|
443
563
|
rawContext: context.executionContext
|
|
444
564
|
};
|
|
445
565
|
}
|
|
446
|
-
/**
|
|
447
|
-
* Extracts the HTTP method from the incoming rawEvent.
|
|
448
|
-
*
|
|
449
|
-
* @param rawEvent - The incoming rawEvent.
|
|
450
|
-
* @returns The HTTP method string.
|
|
451
|
-
*/
|
|
452
|
-
getMethod(rawEvent) {
|
|
453
|
-
return rawEvent.httpMethod ??
|
|
454
|
-
rawEvent.requestContext?.httpMethod ??
|
|
455
|
-
rawEvent.requestContext?.http?.method ??
|
|
456
|
-
'GET';
|
|
457
|
-
}
|
|
458
566
|
/**
|
|
459
567
|
* Extracts proxy-related options from the blueprint.
|
|
460
568
|
*
|
|
@@ -462,7 +570,7 @@ class IncomingEventMiddleware {
|
|
|
462
570
|
*/
|
|
463
571
|
getProxyOptions() {
|
|
464
572
|
const defaultProxyOptions = { trusted: [], trustedIp: [], untrustedIp: [] };
|
|
465
|
-
const proxyOptions = this.blueprint.get('stone.http.proxies', defaultProxyOptions);
|
|
573
|
+
const proxyOptions = { ...this.blueprint.get('stone.http.proxies', defaultProxyOptions) };
|
|
466
574
|
proxyOptions.trusted = this.blueprint.get('stone.http.hosts.trusted', []);
|
|
467
575
|
return proxyOptions;
|
|
468
576
|
}
|
|
@@ -483,58 +591,58 @@ class IncomingEventMiddleware {
|
|
|
483
591
|
return this.blueprint.get('stone.http.cookie.secret', this.blueprint.get('stone.secret', ''));
|
|
484
592
|
}
|
|
485
593
|
/**
|
|
486
|
-
* Extracts and parses the URL from the
|
|
594
|
+
* Extracts and parses the URL (including the query string) from the normalized event.
|
|
487
595
|
*
|
|
488
|
-
* @param
|
|
596
|
+
* @param event - The normalized HTTP event.
|
|
489
597
|
* @param options - Proxy options.
|
|
490
598
|
* @returns The parsed URL object.
|
|
491
599
|
*/
|
|
492
|
-
extractUrl(
|
|
493
|
-
const hostname = getHostname(
|
|
494
|
-
const proto = getProtocol(
|
|
495
|
-
|
|
600
|
+
extractUrl(event, options) {
|
|
601
|
+
const hostname = getHostname(event.sourceIp, event.headers, options);
|
|
602
|
+
const proto = getProtocol(event.sourceIp, event.headers, true, options);
|
|
603
|
+
const pathWithQuery = event.rawQueryString.length > 0 ? `${event.path}?${event.rawQueryString}` : event.path;
|
|
604
|
+
return new URL(pathWithQuery, `${String(proto)}://${String(hostname)}`);
|
|
496
605
|
}
|
|
497
606
|
/**
|
|
498
|
-
* Extracts a list of IP addresses from the
|
|
607
|
+
* Extracts a list of IP addresses from the normalized event.
|
|
499
608
|
*
|
|
500
|
-
* @param
|
|
609
|
+
* @param event - The normalized HTTP event.
|
|
501
610
|
* @param options - Proxy options.
|
|
502
611
|
* @returns An array of IP addresses.
|
|
503
612
|
*/
|
|
504
|
-
extractIpAddresses(
|
|
613
|
+
extractIpAddresses(event, options) {
|
|
505
614
|
const isTrusted = isIpTrusted(options.trustedIp, options.untrustedIp);
|
|
506
|
-
return proxyAddr.all(this.toNodeMessage(
|
|
615
|
+
return proxyAddr.all(this.toNodeMessage(event), isTrusted).slice(1).reverse();
|
|
507
616
|
}
|
|
508
617
|
/**
|
|
509
|
-
* Converts the
|
|
618
|
+
* Converts the normalized event to a minimal Node.js IncomingMessage for `proxy-addr`.
|
|
619
|
+
*
|
|
620
|
+
* All standard forwarding headers are forwarded (not just `x-forwarded-for`) so proxy-addr can
|
|
621
|
+
* honour the deployment's trust configuration.
|
|
510
622
|
*
|
|
511
|
-
* @param
|
|
623
|
+
* @param event - The normalized HTTP event.
|
|
512
624
|
* @returns The converted IncomingMessage.
|
|
513
625
|
*/
|
|
514
|
-
toNodeMessage(
|
|
626
|
+
toNodeMessage(event) {
|
|
515
627
|
return {
|
|
516
|
-
connection: { remoteAddress:
|
|
517
|
-
|
|
628
|
+
connection: { remoteAddress: event.sourceIp },
|
|
629
|
+
socket: { remoteAddress: event.sourceIp },
|
|
630
|
+
headers: {
|
|
631
|
+
forwarded: event.headers.forwarded,
|
|
632
|
+
'x-real-ip': event.headers['x-real-ip'],
|
|
633
|
+
'x-forwarded-for': event.headers['x-forwarded-for'] ?? event.sourceIp
|
|
634
|
+
}
|
|
518
635
|
};
|
|
519
636
|
}
|
|
520
637
|
/**
|
|
521
|
-
* Determines the protocol from the
|
|
638
|
+
* Determines the protocol from the normalized event.
|
|
522
639
|
*
|
|
523
|
-
* @param
|
|
640
|
+
* @param event - The normalized HTTP event.
|
|
524
641
|
* @param options - Proxy options.
|
|
525
642
|
* @returns The protocol string.
|
|
526
643
|
*/
|
|
527
|
-
getProtocol(
|
|
528
|
-
return getProtocol(
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Retrieves the remote address from the incoming rawEvent.
|
|
532
|
-
* This method is used as a fallback when the remote address is not found in the rawEvent.
|
|
533
|
-
* @param rawEvent - The incoming rawEvent.
|
|
534
|
-
* @returns The remote address string.
|
|
535
|
-
*/
|
|
536
|
-
getRemoteAddress(rawEvent) {
|
|
537
|
-
return rawEvent.requestContext?.http?.sourceIp ?? rawEvent.requestContext?.identity?.sourceIp ?? '';
|
|
644
|
+
getProtocol(event, options) {
|
|
645
|
+
return getProtocol(event.sourceIp, event.headers, true, options);
|
|
538
646
|
}
|
|
539
647
|
}
|
|
540
648
|
/**
|
|
@@ -564,6 +672,7 @@ class ServerResponseMiddleware {
|
|
|
564
672
|
}
|
|
565
673
|
rawResponseBuilder
|
|
566
674
|
.add('headers', context.outgoingResponse.headers)
|
|
675
|
+
.add('version', detectEventVersion(context.rawEvent))
|
|
567
676
|
.add('statusCode', context.outgoingResponse.statusCode ?? 500)
|
|
568
677
|
.add('statusMessage', context.outgoingResponse.statusMessage ?? statuses.message[context.outgoingResponse.statusCode ?? 500]);
|
|
569
678
|
if (!context.incomingEvent.isMethod('HEAD')) {
|
|
@@ -579,8 +688,7 @@ class ServerResponseMiddleware {
|
|
|
579
688
|
: context.outgoingResponse.content;
|
|
580
689
|
rawResponseBuilder
|
|
581
690
|
.add('body', content)
|
|
582
|
-
.add('isBase64Encoded', isBuffer)
|
|
583
|
-
.add('charset', context.outgoingResponse.charset);
|
|
691
|
+
.add('isBase64Encoded', isBuffer);
|
|
584
692
|
}
|
|
585
693
|
}
|
|
586
694
|
return rawResponseBuilder;
|
|
@@ -591,6 +699,79 @@ class ServerResponseMiddleware {
|
|
|
591
699
|
*/
|
|
592
700
|
const MetaServerResponseMiddleware = { module: ServerResponseMiddleware, isClass: true };
|
|
593
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Default blueprint configuration for the AWS Lambda Http Adapter.
|
|
704
|
+
*
|
|
705
|
+
* This blueprint defines the initial configuration for the AWS Lambda Http adapter
|
|
706
|
+
* within the Stone.js framework. It includes:
|
|
707
|
+
* - An alias for the AWS Lambda platform (`AWS_LAMBDA_HTTP_PLATFORM`).
|
|
708
|
+
* - A default resolver function (currently a placeholder).
|
|
709
|
+
* - Middleware, hooks, and state flags (`current`, `default`, `preferred`).
|
|
710
|
+
*/
|
|
711
|
+
const awsLambdaHttpAdapterBlueprint = {
|
|
712
|
+
stone: {
|
|
713
|
+
...httpCoreBlueprint.stone,
|
|
714
|
+
blueprint: {
|
|
715
|
+
middleware: metaAdapterBlueprintMiddleware
|
|
716
|
+
},
|
|
717
|
+
adapters: [
|
|
718
|
+
{
|
|
719
|
+
current: false,
|
|
720
|
+
variant: 'server',
|
|
721
|
+
platform: AWS_LAMBDA_HTTP_PLATFORM,
|
|
722
|
+
middleware: [
|
|
723
|
+
MetaIncomingEventMiddleware,
|
|
724
|
+
MetaServerResponseMiddleware
|
|
725
|
+
],
|
|
726
|
+
resolver: awsLambdaHttpAdapterResolver,
|
|
727
|
+
eventHandlerResolver: defaultKernelResolver,
|
|
728
|
+
errorHandlers: {
|
|
729
|
+
default: { module: AwsLambdaHttpErrorHandler, isClass: true }
|
|
730
|
+
},
|
|
731
|
+
default: isNotEmpty(getString('AWS_LAMBDA_FUNCTION_NAME', ''))
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* A Stone.js decorator that integrates the AWS Lambda HTTP Adapter with a class.
|
|
739
|
+
*
|
|
740
|
+
* This decorator modifies the class to seamlessly enable AWS Lambda HTTP as the
|
|
741
|
+
* execution environment for a Stone.js application. By applying this decorator,
|
|
742
|
+
* the class is automatically configured with the necessary blueprint for AWS Lambda HTTP.
|
|
743
|
+
*
|
|
744
|
+
* @template T - The type of the class being decorated. Defaults to `ClassType`.
|
|
745
|
+
* @param options - Optional configuration to customize the AWS Lambda HTTP Adapter.
|
|
746
|
+
*
|
|
747
|
+
* @returns A class decorator that applies the AWS Lambda HTTP adapter configuration.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* import { AwsLambdaHttp } from '@stone-js/aws-lambda-http-adapter';
|
|
752
|
+
*
|
|
753
|
+
* @AwsLambdaHttp({
|
|
754
|
+
* alias: 'MyAwsLambdaHttpAdapter',
|
|
755
|
+
* current: true,
|
|
756
|
+
* })
|
|
757
|
+
* class App {
|
|
758
|
+
* // Your application logic here
|
|
759
|
+
* }
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
const AwsLambdaHttp = (options = {}) => {
|
|
763
|
+
return classDecoratorLegacyWrapper((target, context) => {
|
|
764
|
+
// Clone the module-level default before merging so decorating a class never mutates the shared
|
|
765
|
+
// singleton (which would leak options across classes and tests). cloneValue recreates plain
|
|
766
|
+
// objects/arrays while keeping functions and class references intact.
|
|
767
|
+
const blueprint = cloneValue(awsLambdaHttpAdapterBlueprint);
|
|
768
|
+
if (blueprint.stone?.adapters?.[0] !== undefined) {
|
|
769
|
+
blueprint.stone.adapters[0] = deepMerge(blueprint.stone.adapters[0], options);
|
|
770
|
+
}
|
|
771
|
+
addBlueprint(target, context, blueprint);
|
|
772
|
+
});
|
|
773
|
+
};
|
|
774
|
+
|
|
594
775
|
/**
|
|
595
776
|
* Class representing a BodyEventMiddleware.
|
|
596
777
|
*
|
|
@@ -626,16 +807,33 @@ class BodyEventMiddleware {
|
|
|
626
807
|
}
|
|
627
808
|
if (!isMultipart(this.toNodeMessage(context.rawEvent))) {
|
|
628
809
|
const body = this.getBody(this.toNodeMessage(context.rawEvent), context.rawEvent);
|
|
629
|
-
const method = body
|
|
810
|
+
const method = this.extractSpoofedMethod(body);
|
|
630
811
|
context
|
|
631
812
|
.incomingEventBuilder
|
|
632
813
|
.add('body', body)
|
|
633
|
-
|
|
634
|
-
|
|
814
|
+
// Keep the untouched payload available even after parsing (webhook signatures, etc.).
|
|
815
|
+
.add('metadata', { rawBody: getRawBody(context.rawEvent) });
|
|
816
|
+
// In fullstack forms, the method is spoofed and sent as a hidden field.
|
|
635
817
|
isNotEmpty(method) && context.incomingEventBuilder.add('method', method);
|
|
636
818
|
}
|
|
637
819
|
return await next(context);
|
|
638
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Extract a spoofed HTTP method from a parsed body, supporting both JSON objects and
|
|
823
|
+
* urlencoded bodies (whose fields live on a `URLSearchParams`, not as own properties).
|
|
824
|
+
*
|
|
825
|
+
* @param body - The parsed body.
|
|
826
|
+
* @returns The spoofed method, or undefined.
|
|
827
|
+
*/
|
|
828
|
+
extractSpoofedMethod(body) {
|
|
829
|
+
if (body instanceof URLSearchParams) {
|
|
830
|
+
return body.get('$method$') ?? undefined;
|
|
831
|
+
}
|
|
832
|
+
if (typeof body === 'object' && body !== null) {
|
|
833
|
+
return body.$method$;
|
|
834
|
+
}
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
639
837
|
/**
|
|
640
838
|
* Convert the raw event into a Node.js IncomingMessage.
|
|
641
839
|
*
|
|
@@ -643,11 +841,12 @@ class BodyEventMiddleware {
|
|
|
643
841
|
* @returns The converted IncomingMessage.
|
|
644
842
|
*/
|
|
645
843
|
toNodeMessage(rawEvent) {
|
|
844
|
+
const headers = normalizeHeaders(rawEvent);
|
|
646
845
|
return {
|
|
647
846
|
headers: {
|
|
648
|
-
'content-type':
|
|
649
|
-
'content-length':
|
|
650
|
-
'transfer-encoding':
|
|
847
|
+
'content-type': headers['content-type'],
|
|
848
|
+
'content-length': headers['content-length'],
|
|
849
|
+
'transfer-encoding': headers['transfer-encoding']
|
|
651
850
|
}
|
|
652
851
|
};
|
|
653
852
|
}
|
|
@@ -667,50 +866,54 @@ class BodyEventMiddleware {
|
|
|
667
866
|
const limit = bytes.parse(rawLimit) ?? 100000;
|
|
668
867
|
const encoding = getCharset(message, defaultCharset);
|
|
669
868
|
const type = typeIs(message, ['urlencoded', 'json', 'text', 'bin']) ?? defaultType;
|
|
670
|
-
|
|
671
|
-
|
|
869
|
+
// Decode ONCE to a Buffer. Binary payloads must never round-trip through a lossy UTF-8 string
|
|
870
|
+
// (which corrupts any non-UTF-8 byte); the limit is measured on the true byte length.
|
|
871
|
+
const rawBuffer = this.getRawBuffer(rawEvent, encoding);
|
|
872
|
+
if (rawBuffer.byteLength > limit) {
|
|
672
873
|
throw new AwsLambdaHttpAdapterError('Body payload exceeds configured limit.');
|
|
673
874
|
}
|
|
674
|
-
return this.parseBodyContent(type,
|
|
875
|
+
return this.parseBodyContent(type, rawBuffer, encoding);
|
|
675
876
|
}
|
|
676
877
|
/**
|
|
677
|
-
*
|
|
878
|
+
* Decode the request body into a Buffer, honouring base64 encoding.
|
|
678
879
|
*
|
|
679
880
|
* @param rawEvent - The raw event containing the body.
|
|
680
|
-
* @param encoding - The
|
|
681
|
-
* @returns The
|
|
881
|
+
* @param encoding - The charset for a plain-text (non-base64) body.
|
|
882
|
+
* @returns The body as a Buffer.
|
|
682
883
|
*/
|
|
683
|
-
|
|
884
|
+
getRawBuffer(rawEvent, encoding) {
|
|
684
885
|
if (typeof rawEvent.body === 'string') {
|
|
685
886
|
return rawEvent.isBase64Encoded === true
|
|
686
|
-
? Buffer.from(rawEvent.body, 'base64')
|
|
687
|
-
: rawEvent.body;
|
|
887
|
+
? Buffer.from(rawEvent.body, 'base64')
|
|
888
|
+
: Buffer.from(rawEvent.body, encoding);
|
|
688
889
|
}
|
|
689
890
|
if (typeof rawEvent.body === 'object' && rawEvent.body !== null) {
|
|
690
|
-
return JSON.stringify(rawEvent.body);
|
|
891
|
+
return Buffer.from(JSON.stringify(rawEvent.body), encoding);
|
|
691
892
|
}
|
|
692
|
-
return
|
|
893
|
+
return Buffer.alloc(0);
|
|
693
894
|
}
|
|
694
895
|
/**
|
|
695
896
|
* Parse the body content based on the specified type and encoding.
|
|
696
897
|
*
|
|
697
898
|
* @param type - The content type of the body.
|
|
698
|
-
* @param
|
|
899
|
+
* @param buffer - The raw body content as a Buffer.
|
|
699
900
|
* @param encoding - The encoding of the body content.
|
|
700
901
|
* @returns The parsed body content as an object, string, or Buffer.
|
|
701
902
|
* @throws {AwsLambdaHttpAdapterError} If parsing fails.
|
|
702
903
|
*/
|
|
703
|
-
parseBodyContent(type,
|
|
904
|
+
parseBodyContent(type, buffer, encoding) {
|
|
704
905
|
try {
|
|
705
906
|
switch (type) {
|
|
706
|
-
case 'json':
|
|
707
|
-
|
|
907
|
+
case 'json': {
|
|
908
|
+
const text = buffer.toString(encoding);
|
|
909
|
+
return isNotEmpty(text) ? JSON.parse(text) : {};
|
|
910
|
+
}
|
|
708
911
|
case 'text':
|
|
709
|
-
return
|
|
912
|
+
return buffer.toString(encoding);
|
|
710
913
|
case 'urlencoded':
|
|
711
|
-
return new URLSearchParams(
|
|
914
|
+
return new URLSearchParams(buffer.toString(encoding));
|
|
712
915
|
case 'bin':
|
|
713
|
-
return
|
|
916
|
+
return buffer; // Return the raw bytes untouched — no lossy re-encoding.
|
|
714
917
|
default:
|
|
715
918
|
return {};
|
|
716
919
|
}
|
|
@@ -725,223 +928,79 @@ class BodyEventMiddleware {
|
|
|
725
928
|
*/
|
|
726
929
|
const MetaBodyEventMiddleware = { module: BodyEventMiddleware, isClass: true };
|
|
727
930
|
|
|
728
|
-
function getDefaultExportFromCjs (x) {
|
|
729
|
-
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
var cjs;
|
|
733
|
-
var hasRequiredCjs;
|
|
734
|
-
|
|
735
|
-
function requireCjs () {
|
|
736
|
-
if (hasRequiredCjs) return cjs;
|
|
737
|
-
hasRequiredCjs = 1;
|
|
738
|
-
|
|
739
|
-
var isMergeableObject = function isMergeableObject(value) {
|
|
740
|
-
return isNonNullObject(value)
|
|
741
|
-
&& !isSpecial(value)
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
function isNonNullObject(value) {
|
|
745
|
-
return !!value && typeof value === 'object'
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function isSpecial(value) {
|
|
749
|
-
var stringValue = Object.prototype.toString.call(value);
|
|
750
|
-
|
|
751
|
-
return stringValue === '[object RegExp]'
|
|
752
|
-
|| stringValue === '[object Date]'
|
|
753
|
-
|| isReactElement(value)
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
|
757
|
-
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
|
758
|
-
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
|
759
|
-
|
|
760
|
-
function isReactElement(value) {
|
|
761
|
-
return value.$$typeof === REACT_ELEMENT_TYPE
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function emptyTarget(val) {
|
|
765
|
-
return Array.isArray(val) ? [] : {}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function cloneUnlessOtherwiseSpecified(value, options) {
|
|
769
|
-
return (options.clone !== false && options.isMergeableObject(value))
|
|
770
|
-
? deepmerge(emptyTarget(value), value, options)
|
|
771
|
-
: value
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function defaultArrayMerge(target, source, options) {
|
|
775
|
-
return target.concat(source).map(function(element) {
|
|
776
|
-
return cloneUnlessOtherwiseSpecified(element, options)
|
|
777
|
-
})
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function getMergeFunction(key, options) {
|
|
781
|
-
if (!options.customMerge) {
|
|
782
|
-
return deepmerge
|
|
783
|
-
}
|
|
784
|
-
var customMerge = options.customMerge(key);
|
|
785
|
-
return typeof customMerge === 'function' ? customMerge : deepmerge
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function getEnumerableOwnPropertySymbols(target) {
|
|
789
|
-
return Object.getOwnPropertySymbols
|
|
790
|
-
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
|
|
791
|
-
return Object.propertyIsEnumerable.call(target, symbol)
|
|
792
|
-
})
|
|
793
|
-
: []
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function getKeys(target) {
|
|
797
|
-
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function propertyIsOnObject(object, property) {
|
|
801
|
-
try {
|
|
802
|
-
return property in object
|
|
803
|
-
} catch(_) {
|
|
804
|
-
return false
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
|
809
|
-
function propertyIsUnsafe(target, key) {
|
|
810
|
-
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
|
811
|
-
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
|
812
|
-
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function mergeObject(target, source, options) {
|
|
816
|
-
var destination = {};
|
|
817
|
-
if (options.isMergeableObject(target)) {
|
|
818
|
-
getKeys(target).forEach(function(key) {
|
|
819
|
-
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
getKeys(source).forEach(function(key) {
|
|
823
|
-
if (propertyIsUnsafe(target, key)) {
|
|
824
|
-
return
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
|
828
|
-
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
|
829
|
-
} else {
|
|
830
|
-
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
return destination
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function deepmerge(target, source, options) {
|
|
837
|
-
options = options || {};
|
|
838
|
-
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
|
839
|
-
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
|
840
|
-
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
|
841
|
-
// implementations can use it. The caller may not replace it.
|
|
842
|
-
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
|
843
|
-
|
|
844
|
-
var sourceIsArray = Array.isArray(source);
|
|
845
|
-
var targetIsArray = Array.isArray(target);
|
|
846
|
-
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
|
847
|
-
|
|
848
|
-
if (!sourceAndTargetTypesMatch) {
|
|
849
|
-
return cloneUnlessOtherwiseSpecified(source, options)
|
|
850
|
-
} else if (sourceIsArray) {
|
|
851
|
-
return options.arrayMerge(target, source, options)
|
|
852
|
-
} else {
|
|
853
|
-
return mergeObject(target, source, options)
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
deepmerge.all = function deepmergeAll(array, options) {
|
|
858
|
-
if (!Array.isArray(array)) {
|
|
859
|
-
throw new Error('first argument should be an array')
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return array.reduce(function(prev, next) {
|
|
863
|
-
return deepmerge(prev, next, options)
|
|
864
|
-
}, {})
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
var deepmerge_1 = deepmerge;
|
|
868
|
-
|
|
869
|
-
cjs = deepmerge_1;
|
|
870
|
-
return cjs;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
var cjsExports = requireCjs();
|
|
874
|
-
var deepmerge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports);
|
|
875
|
-
|
|
876
931
|
/**
|
|
877
|
-
*
|
|
932
|
+
* Class representing a FilesEventMiddleware.
|
|
878
933
|
*
|
|
879
|
-
*
|
|
880
|
-
* within the Stone.js framework. It includes:
|
|
881
|
-
* - An alias for the AWS Lambda platform (`AWS_LAMBDA_HTTP_PLATFORM`).
|
|
882
|
-
* - A default resolver function (currently a placeholder).
|
|
883
|
-
* - Middleware, hooks, and state flags (`current`, `default`, `preferred`).
|
|
934
|
+
* @author Mr. Stone <evensstone@gmail.com>
|
|
884
935
|
*/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
936
|
+
class FilesEventMiddleware {
|
|
937
|
+
/**
|
|
938
|
+
* The blueprint for resolving configuration and dependencies.
|
|
939
|
+
*/
|
|
940
|
+
blueprint;
|
|
941
|
+
/**
|
|
942
|
+
* Create a FilesEventMiddleware.
|
|
943
|
+
*
|
|
944
|
+
* @param {blueprint} options - Options for creating the FilesEventMiddleware.
|
|
945
|
+
*/
|
|
946
|
+
constructor({ blueprint }) {
|
|
947
|
+
this.blueprint = blueprint;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Handles the incoming event, processes it, and invokes the next middleware in the pipeline.
|
|
951
|
+
*
|
|
952
|
+
* @param context - The adapter context containing the raw event, execution context, and other data.
|
|
953
|
+
* @param next - The next middleware to be invoked in the pipeline.
|
|
954
|
+
* @returns A promise that resolves to the destination type after processing.
|
|
955
|
+
*
|
|
956
|
+
* @throws {AwsLambdaHttpAdapterError} If required components such as the rawEvent or IncomingEventBuilder are not provided.
|
|
957
|
+
*/
|
|
958
|
+
async handle(context, next) {
|
|
959
|
+
if (context.rawEvent === undefined || context.incomingEventBuilder?.add === undefined) {
|
|
960
|
+
throw new AwsLambdaHttpAdapterError('The context is missing required components.');
|
|
961
|
+
}
|
|
962
|
+
if (isMultipart(this.normalizeEvent(context.rawEvent))) {
|
|
963
|
+
const options = this.blueprint.get('stone.http.files.upload', {});
|
|
964
|
+
const response = await getFilesUploads(this.normalizeEvent(context.rawEvent), options);
|
|
965
|
+
const method = response.fields.$method$;
|
|
966
|
+
context
|
|
967
|
+
.incomingEventBuilder
|
|
968
|
+
.add('files', response.files)
|
|
969
|
+
.add('body', response.fields)
|
|
970
|
+
// Keep the untouched multipart payload available (webhook signatures, re-streaming, etc.).
|
|
971
|
+
.add('metadata', { rawBody: getRawBody(context.rawEvent) });
|
|
972
|
+
// In fullstack forms, the method is spoofed and sent as a hidden field.
|
|
973
|
+
isNotEmpty(method) && context.incomingEventBuilder.add('method', method);
|
|
974
|
+
}
|
|
975
|
+
return await next(context);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Normalize the incoming event to an IncomingMessage.
|
|
979
|
+
*
|
|
980
|
+
* @param rawEvent - The raw event to be normalized.
|
|
981
|
+
* @returns The normalized event.
|
|
982
|
+
*/
|
|
983
|
+
normalizeEvent(rawEvent) {
|
|
984
|
+
const headers = normalizeHeaders(rawEvent);
|
|
985
|
+
let body = rawEvent.body;
|
|
986
|
+
if (typeof rawEvent.body === 'string') {
|
|
987
|
+
body = rawEvent.isBase64Encoded === true
|
|
988
|
+
? Buffer.from(rawEvent.body, 'base64')
|
|
989
|
+
: Buffer.from(rawEvent.body, 'utf-8');
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
body,
|
|
993
|
+
headers: {
|
|
994
|
+
'content-type': headers['content-type'],
|
|
995
|
+
'content-length': headers['content-length'],
|
|
996
|
+
'transfer-encoding': headers['transfer-encoding']
|
|
906
997
|
}
|
|
907
|
-
|
|
998
|
+
};
|
|
908
999
|
}
|
|
909
|
-
}
|
|
910
|
-
|
|
1000
|
+
}
|
|
911
1001
|
/**
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
* This decorator modifies the class to seamlessly enable AWS Lambda HTTP as the
|
|
915
|
-
* execution environment for a Stone.js application. By applying this decorator,
|
|
916
|
-
* the class is automatically configured with the necessary blueprint for AWS Lambda HTTP.
|
|
917
|
-
*
|
|
918
|
-
* @template T - The type of the class being decorated. Defaults to `ClassType`.
|
|
919
|
-
* @param options - Optional configuration to customize the AWS Lambda HTTP Adapter.
|
|
920
|
-
*
|
|
921
|
-
* @returns A class decorator that applies the AWS Lambda HTTP adapter configuration.
|
|
922
|
-
*
|
|
923
|
-
* @example
|
|
924
|
-
* ```typescript
|
|
925
|
-
* import { AwsLambdaHttp } from '@stone-js/aws-lambda-http-adapter';
|
|
926
|
-
*
|
|
927
|
-
* @AwsLambdaHttp({
|
|
928
|
-
* alias: 'MyAwsLambdaHttpAdapter',
|
|
929
|
-
* current: true,
|
|
930
|
-
* })
|
|
931
|
-
* class App {
|
|
932
|
-
* // Your application logic here
|
|
933
|
-
* }
|
|
934
|
-
* ```
|
|
1002
|
+
* Meta Middleware for processing files uploads.
|
|
935
1003
|
*/
|
|
936
|
-
const
|
|
937
|
-
return classDecoratorLegacyWrapper((target, context) => {
|
|
938
|
-
if (awsLambdaHttpAdapterBlueprint.stone?.adapters?.[0] !== undefined) {
|
|
939
|
-
// Merge provided options with the default AWS Lambda HTTP adapter blueprint.
|
|
940
|
-
awsLambdaHttpAdapterBlueprint.stone.adapters[0] = deepmerge(awsLambdaHttpAdapterBlueprint.stone.adapters[0], options);
|
|
941
|
-
}
|
|
942
|
-
// Add the modified blueprint to the target class.
|
|
943
|
-
addBlueprint(target, context, awsLambdaHttpAdapterBlueprint);
|
|
944
|
-
});
|
|
945
|
-
};
|
|
1004
|
+
const MetaFilesEventMiddleware = { module: FilesEventMiddleware, isClass: true };
|
|
946
1005
|
|
|
947
|
-
export { AWS_LAMBDA_HTTP_PLATFORM, AwsLambdaHttp, AwsLambdaHttpAdapter, AwsLambdaHttpAdapterError, AwsLambdaHttpErrorHandler, BodyEventMiddleware, FilesEventMiddleware, IncomingEventMiddleware, MetaBodyEventMiddleware, MetaFilesEventMiddleware, MetaIncomingEventMiddleware, MetaServerResponseMiddleware, RawHttpResponseWrapper, ServerResponseMiddleware, SetAwsLambdaHttpResponseResolverMiddleware, awsLambdaHttpAdapterBlueprint, awsLambdaHttpAdapterResolver, metaAdapterBlueprintMiddleware };
|
|
1006
|
+
export { AWS_LAMBDA_HTTP_PLATFORM, AwsLambdaHttp, AwsLambdaHttpAdapter, AwsLambdaHttpAdapterError, AwsLambdaHttpErrorHandler, BodyEventMiddleware, FilesEventMiddleware, IncomingEventMiddleware, MetaBodyEventMiddleware, MetaFilesEventMiddleware, MetaIncomingEventMiddleware, MetaServerResponseMiddleware, RawHttpResponseWrapper, ServerResponseMiddleware, SetAwsLambdaHttpResponseResolverMiddleware, awsLambdaHttpAdapterBlueprint, awsLambdaHttpAdapterResolver, buildRawQueryString, collectCookies, detectEventVersion, getRawBody, metaAdapterBlueprintMiddleware, normalizeHeaders, normalizeHttpEvent, resolveSourceIp };
|