@stone-js/aws-lambda-http-adapter 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,889 @@
1
+ import { IntegrationError, Adapter, AdapterEventBuilder, defaultLoggerResolver, isNotEmpty, defaultKernelResolver, classDecoratorLegacyWrapper, addBlueprint } from '@stone-js/core';
2
+ import { IncomingHttpEvent, HTTP_INTERNAL_SERVER_ERROR, BinaryFileResponse, OutgoingHttpResponse, CookieCollection, isIpTrusted, getHostname, getProtocol, httpCoreBlueprint, isMultipart, getCharset, getType, getFilesUploads } from '@stone-js/http-core';
3
+ import mime from 'mime';
4
+ import accepts from 'accepts';
5
+ import statuses from 'statuses';
6
+ import { getString } from '@stone-js/env';
7
+ import { File } from '@stone-js/filesystem';
8
+ import proxyAddr from 'proxy-addr';
9
+ import bytes from 'bytes';
10
+ import typeIs from 'type-is';
11
+
12
+ /**
13
+ * Wrapper for HTTP raw responses in AWS Lambda.
14
+ *
15
+ * The `RawHttpResponseWrapper` is responsible for constructing and returning
16
+ * a raw HTTP response that conforms to the expected structure for AWS Lambda.
17
+ * It implements the `IRawResponseWrapper` interface, ensuring compatibility
18
+ * with the Stone.js framework.
19
+ */
20
+ class RawHttpResponseWrapper {
21
+ options;
22
+ /**
23
+ * Factory method to create an instance of `RawHttpResponseWrapper`.
24
+ *
25
+ * This method accepts partial response options, allowing the user to configure
26
+ * only the required fields. It initializes the wrapper with these options.
27
+ *
28
+ * @param options - Partial options to configure the HTTP response.
29
+ * @returns A new instance of `RawHttpResponseWrapper`.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const responseWrapper = RawHttpResponseWrapper.create({
34
+ * statusCode: 200,
35
+ * body: { message: 'Success' },
36
+ * headers: { 'Content-Type': 'application/json' }
37
+ * });
38
+ *
39
+ * const response = responseWrapper.respond();
40
+ * console.log(response); // { statusCode: 200, body: '{"message":"Success"}', headers: { 'Content-Type': 'application/json' } }
41
+ * ```
42
+ */
43
+ static create(options) {
44
+ return new this(options);
45
+ }
46
+ /**
47
+ * Constructs an instance of `RawHttpResponseWrapper`.
48
+ *
49
+ * This constructor is private and should not be called directly.
50
+ * Use the `create` method to initialize an instance.
51
+ *
52
+ * @param options - Partial options for configuring the HTTP response.
53
+ */
54
+ constructor(options) {
55
+ this.options = options;
56
+ }
57
+ /**
58
+ * Constructs and returns the raw HTTP response.
59
+ *
60
+ * The `respond` method generates a `RawHttpResponse` object based on the
61
+ * provided options. If any required fields are missing, it assigns default values:
62
+ * - `statusCode`: Defaults to `500`.
63
+ *
64
+ * @returns A `RawHttpResponse` object.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const responseWrapper = RawHttpResponseWrapper.create({ body: 'Hello, world!', statusCode: 200 });
69
+ * const response = responseWrapper.respond();
70
+ * console.log(response); // { statusCode: 500, statusMessage: '', body: 'Hello, world!', headers: undefined }
71
+ * ```
72
+ */
73
+ respond() {
74
+ return {
75
+ ...this.options,
76
+ statusCode: this.options.statusCode ?? 500
77
+ };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Custom error for AWS Lambda adapter operations.
83
+ */
84
+ class AwsLambdaHttpAdapterError extends IntegrationError {
85
+ constructor(message, options) {
86
+ super(message, options);
87
+ this.name = 'AwsLambdaHttpAdapterError';
88
+ }
89
+ }
90
+
91
+ /**
92
+ * AWS Lambda HTTP Adapter for Stone.js.
93
+ *
94
+ * The `AwsLambdaHttpAdapter` extends the functionality of the Stone.js `Adapter`
95
+ * to provide seamless integration with AWS Lambda for HTTP-based events. This adapter
96
+ * transforms incoming HTTP events from AWS Lambda into `IncomingHttpEvent` instances
97
+ * and produces a `RawHttpResponse` as output.
98
+ *
99
+ * This adapter simplifies the process of handling HTTP events within AWS Lambda
100
+ * while adhering to the Stone.js framework's event-driven architecture.
101
+ *
102
+ * @template AwsLambdaHttpEvent - The type of the raw HTTP event from AWS Lambda.
103
+ * @template RawHttpResponse - The type of the raw HTTP response to send back.
104
+ * @template AwsLambdaContext - The AWS Lambda execution context type.
105
+ * @template IncomingHttpEvent - The type of the processed incoming HTTP event.
106
+ * @template IncomingHttpEventOptions - Options used to create an incoming HTTP event.
107
+ * @template OutgoingHttpResponse - The type of the outgoing HTTP response after processing.
108
+ * @template AwsLambdaHttpAdapterContext - Context type specific to the HTTP adapter.
109
+ *
110
+ * @extends Adapter
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * import { AwsLambdaHttpAdapter } from '@stone-js/aws-lambda-http-adapter';
115
+ *
116
+ * const adapter = AwsLambdaHttpAdapter.create({...});
117
+ *
118
+ * const handler = await adapter.run();
119
+ *
120
+ * export { handler };
121
+ * ```
122
+ *
123
+ * @see {@link https://stone-js.com/docs Stone.js Documentation}
124
+ * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/ AWS Lambda Documentation}
125
+ */
126
+ class AwsLambdaHttpAdapter extends Adapter {
127
+ /**
128
+ * Creates an instance of the `AwsLambdaHttpAdapter`.
129
+ *
130
+ * @param blueprint - The application blueprint.
131
+ * @returns A new instance of `AwsLambdaHttpAdapter`.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const adapter = AwsLambdaHttpAdapter.create(blueprint);
136
+ * await adapter.run();
137
+ * ```
138
+ */
139
+ static create(blueprint) {
140
+ return new this(blueprint);
141
+ }
142
+ /**
143
+ * Executes the adapter and provides an AWS Lambda-compatible HTTP handler function.
144
+ *
145
+ * This method initializes the adapter and returns a handler function that can
146
+ * process HTTP events in AWS Lambda. It transforms raw events into `IncomingHttpEvent`
147
+ * instances and produces `RawHttpResponse` objects as output.
148
+ *
149
+ * @template ExecutionResultType - The type representing the AWS Lambda event handler function.
150
+ * @returns A promise resolving to the AWS Lambda HTTP handler function.
151
+ * @throws {AwsLambdaHttpAdapterError} If used outside the AWS Lambda environment.
152
+ */
153
+ async run() {
154
+ await this.onStart();
155
+ const handler = async (rawEvent, executionContext) => {
156
+ return await this.eventListener(rawEvent, executionContext);
157
+ };
158
+ return handler;
159
+ }
160
+ /**
161
+ * Initializes the adapter and validates its execution context.
162
+ *
163
+ * Ensures that the adapter is running in an AWS Lambda environment. Throws an error
164
+ * if it detects that the adapter is being used in an unsupported environment (e.g., a browser).
165
+ *
166
+ * @throws {AwsLambdaHttpAdapterError} If executed outside an AWS Lambda environment.
167
+ */
168
+ async onStart() {
169
+ if (typeof window === 'object') {
170
+ throw new AwsLambdaHttpAdapterError('This `AWSLambdaAdapter` must be used only in AWS Lambda context.');
171
+ }
172
+ await this.executeHooks('onStart');
173
+ }
174
+ /**
175
+ * Processes an incoming AWS Lambda HTTP event.
176
+ *
177
+ * Converts a raw AWS Lambda HTTP event into an `IncomingHttpEvent`, processes it through
178
+ * the Stone.js pipeline, and generates a `RawHttpResponse` to send back.
179
+ *
180
+ * @param rawEvent - The raw HTTP event received from AWS Lambda.
181
+ * @param executionContext - The AWS Lambda execution context associated with the event.
182
+ * @returns A promise resolving to the processed `RawHttpResponse`.
183
+ */
184
+ async eventListener(rawEvent, executionContext) {
185
+ const incomingEventBuilder = AdapterEventBuilder.create({
186
+ resolver: (options) => IncomingHttpEvent.create(options)
187
+ });
188
+ const rawResponseBuilder = AdapterEventBuilder.create({
189
+ resolver: (options) => RawHttpResponseWrapper.create(options)
190
+ });
191
+ const rawResponse = { statusCode: 500 };
192
+ const context = {
193
+ rawEvent,
194
+ rawResponse,
195
+ executionContext,
196
+ rawResponseBuilder,
197
+ incomingEventBuilder
198
+ };
199
+ try {
200
+ const eventHandler = this.resolveEventHandler();
201
+ await this.executeEventHandlerHooks('onInit', eventHandler);
202
+ return await this.sendEventThroughDestination(context, eventHandler);
203
+ }
204
+ catch (error) {
205
+ const rawResponseBuilder = await this.handleError(error, context);
206
+ return await this.buildRawResponse({ ...context, rawResponseBuilder });
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Class representing an AwsLambdaHttpErrorHandler.
213
+ */
214
+ class AwsLambdaHttpErrorHandler {
215
+ logger;
216
+ /**
217
+ * Create an NodeHttpErrorHandler.
218
+ *
219
+ * @param options - NodeHttpErrorHandler options.
220
+ */
221
+ constructor({ blueprint }) {
222
+ this.logger = blueprint.get('stone.logger.resolver', defaultLoggerResolver)(blueprint);
223
+ }
224
+ /**
225
+ * Handle an error.
226
+ *
227
+ * @param error - The error to handle.
228
+ * @param context - The context of the adapter.
229
+ * @returns The raw response builder.
230
+ */
231
+ handle(error, context) {
232
+ this.logger.error(error.message, { error });
233
+ const statusCode = error.cause?.status ?? HTTP_INTERNAL_SERVER_ERROR;
234
+ const type = accepts(context.rawEvent).type(['json', 'html']);
235
+ const contentType = mime.getType(type !== false ? type : 'txt') ?? context.rawEvent.headers['content-type'] ?? 'text/plain';
236
+ const headers = new Headers({ 'Content-Type': contentType });
237
+ return context
238
+ .rawResponseBuilder
239
+ .add('headers', headers)
240
+ .add('statusCode', statusCode)
241
+ .add('statusMessage', statuses.message[statusCode]);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * A constant representing the AWS Lambda HTTP platform identifier.
247
+ *
248
+ * This constant is used as an alias for the AWS Lambda HTTP Adapter within the Stone.js framework.
249
+ * It helps in identifying and configuring platform-specific adapters or components for handling
250
+ * HTTP requests and responses.
251
+ */
252
+ const AWS_LAMBDA_HTTP_PLATFORM = 'aws_lambda_http';
253
+
254
+ /**
255
+ * Adapter resolver for AWS Lambda HTTP adapter.
256
+ *
257
+ * Creates and configures an `AWSLambdaHttpAdapter` for handling HTTP events in AWS Lambda.
258
+ *
259
+ * @param blueprint - The `IBlueprint` providing configuration and dependencies.
260
+ * @returns An `AWSLambdaHttpAdapter` instance.
261
+ */
262
+ const awsLambdaHttpAdapterResolver = (blueprint) => {
263
+ return AwsLambdaHttpAdapter.create(blueprint);
264
+ };
265
+
266
+ function getDefaultExportFromCjs (x) {
267
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
268
+ }
269
+
270
+ var cjs;
271
+ var hasRequiredCjs;
272
+
273
+ function requireCjs () {
274
+ if (hasRequiredCjs) return cjs;
275
+ hasRequiredCjs = 1;
276
+
277
+ var isMergeableObject = function isMergeableObject(value) {
278
+ return isNonNullObject(value)
279
+ && !isSpecial(value)
280
+ };
281
+
282
+ function isNonNullObject(value) {
283
+ return !!value && typeof value === 'object'
284
+ }
285
+
286
+ function isSpecial(value) {
287
+ var stringValue = Object.prototype.toString.call(value);
288
+
289
+ return stringValue === '[object RegExp]'
290
+ || stringValue === '[object Date]'
291
+ || isReactElement(value)
292
+ }
293
+
294
+ // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
295
+ var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
296
+ var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
297
+
298
+ function isReactElement(value) {
299
+ return value.$$typeof === REACT_ELEMENT_TYPE
300
+ }
301
+
302
+ function emptyTarget(val) {
303
+ return Array.isArray(val) ? [] : {}
304
+ }
305
+
306
+ function cloneUnlessOtherwiseSpecified(value, options) {
307
+ return (options.clone !== false && options.isMergeableObject(value))
308
+ ? deepmerge(emptyTarget(value), value, options)
309
+ : value
310
+ }
311
+
312
+ function defaultArrayMerge(target, source, options) {
313
+ return target.concat(source).map(function(element) {
314
+ return cloneUnlessOtherwiseSpecified(element, options)
315
+ })
316
+ }
317
+
318
+ function getMergeFunction(key, options) {
319
+ if (!options.customMerge) {
320
+ return deepmerge
321
+ }
322
+ var customMerge = options.customMerge(key);
323
+ return typeof customMerge === 'function' ? customMerge : deepmerge
324
+ }
325
+
326
+ function getEnumerableOwnPropertySymbols(target) {
327
+ return Object.getOwnPropertySymbols
328
+ ? Object.getOwnPropertySymbols(target).filter(function(symbol) {
329
+ return Object.propertyIsEnumerable.call(target, symbol)
330
+ })
331
+ : []
332
+ }
333
+
334
+ function getKeys(target) {
335
+ return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
336
+ }
337
+
338
+ function propertyIsOnObject(object, property) {
339
+ try {
340
+ return property in object
341
+ } catch(_) {
342
+ return false
343
+ }
344
+ }
345
+
346
+ // Protects from prototype poisoning and unexpected merging up the prototype chain.
347
+ function propertyIsUnsafe(target, key) {
348
+ return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
349
+ && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
350
+ && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
351
+ }
352
+
353
+ function mergeObject(target, source, options) {
354
+ var destination = {};
355
+ if (options.isMergeableObject(target)) {
356
+ getKeys(target).forEach(function(key) {
357
+ destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
358
+ });
359
+ }
360
+ getKeys(source).forEach(function(key) {
361
+ if (propertyIsUnsafe(target, key)) {
362
+ return
363
+ }
364
+
365
+ if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
366
+ destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
367
+ } else {
368
+ destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
369
+ }
370
+ });
371
+ return destination
372
+ }
373
+
374
+ function deepmerge(target, source, options) {
375
+ options = options || {};
376
+ options.arrayMerge = options.arrayMerge || defaultArrayMerge;
377
+ options.isMergeableObject = options.isMergeableObject || isMergeableObject;
378
+ // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
379
+ // implementations can use it. The caller may not replace it.
380
+ options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
381
+
382
+ var sourceIsArray = Array.isArray(source);
383
+ var targetIsArray = Array.isArray(target);
384
+ var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
385
+
386
+ if (!sourceAndTargetTypesMatch) {
387
+ return cloneUnlessOtherwiseSpecified(source, options)
388
+ } else if (sourceIsArray) {
389
+ return options.arrayMerge(target, source, options)
390
+ } else {
391
+ return mergeObject(target, source, options)
392
+ }
393
+ }
394
+
395
+ deepmerge.all = function deepmergeAll(array, options) {
396
+ if (!Array.isArray(array)) {
397
+ throw new Error('first argument should be an array')
398
+ }
399
+
400
+ return array.reduce(function(prev, next) {
401
+ return deepmerge(prev, next, options)
402
+ }, {})
403
+ };
404
+
405
+ var deepmerge_1 = deepmerge;
406
+
407
+ cjs = deepmerge_1;
408
+ return cjs;
409
+ }
410
+
411
+ var cjsExports = requireCjs();
412
+ var deepmerge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports);
413
+
414
+ /**
415
+ * Middleware to dynamically set response resolver for adapter.
416
+ *
417
+ * @param context - The configuration context containing modules and blueprint.
418
+ * @param next - The next pipeline function to continue processing.
419
+ * @returns The updated blueprint or a promise resolving to it.
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * SetAwsLambdaHttpResponseResolverMiddleware(context, next)
424
+ * ```
425
+ */
426
+ const SetAwsLambdaHttpResponseResolverMiddleware = async (context, next) => {
427
+ if (context.blueprint.get('stone.adapter.platform') === AWS_LAMBDA_HTTP_PLATFORM) {
428
+ context.blueprint.set('stone.kernel.responseResolver', (options) => {
429
+ return options.content instanceof File
430
+ ? BinaryFileResponse.file({ ...options, content: undefined, file: options.content })
431
+ : OutgoingHttpResponse.create(options);
432
+ });
433
+ }
434
+ return await next(context);
435
+ };
436
+ /**
437
+ * Configuration for adapter processing middleware.
438
+ *
439
+ * This array defines a list of middleware pipes, each with a `pipe` function and a `priority`.
440
+ * These pipes are executed in the order of their priority values, with lower values running first.
441
+ */
442
+ const metaAdapterBlueprintMiddleware = [
443
+ { module: SetAwsLambdaHttpResponseResolverMiddleware, priority: 6 }
444
+ ];
445
+
446
+ /**
447
+ * Middleware for handling incoming events and transforming them into Stone.js events.
448
+ *
449
+ * This class processes incoming HTTP requests, extracting relevant data such as URL, IP addresses,
450
+ * headers, cookies, and more, and forwards them to the next middleware in the pipeline.
451
+ */
452
+ class IncomingEventMiddleware {
453
+ /**
454
+ * The blueprint for resolving configuration and dependencies.
455
+ */
456
+ blueprint;
457
+ /**
458
+ * Create an IncomingEventMiddleware instance.
459
+ *
460
+ * @param {blueprint} options - Options containing the blueprint for resolving configuration and dependencies.
461
+ */
462
+ constructor({ blueprint }) {
463
+ this.blueprint = blueprint;
464
+ }
465
+ /**
466
+ * Handles the incoming event, processes it, and invokes the next middleware in the pipeline.
467
+ *
468
+ * @param context - The adapter context containing the raw event, execution context, and other data.
469
+ * @param next - The next middleware to be invoked in the pipeline.
470
+ * @returns A promise that resolves to the processed context.
471
+ * @throws {AwsLambdaHttpAdapterError} If required components are missing in the context.
472
+ */
473
+ async handle(context, next) {
474
+ if ((context.rawEvent === undefined) || ((context.incomingEventBuilder?.add) === undefined)) {
475
+ throw new AwsLambdaHttpAdapterError('The context is missing required components.');
476
+ }
477
+ const proxyOptions = this.getProxyOptions();
478
+ const cookieOptions = this.getCookieOptions();
479
+ const url = this.extractUrl(context.rawEvent, proxyOptions);
480
+ const ipAddresses = this.extractIpAddresses(context.rawEvent, proxyOptions);
481
+ context
482
+ .incomingEventBuilder
483
+ .add('url', url)
484
+ .add('ips', ipAddresses)
485
+ .add('source', this.getSource(context))
486
+ .add('headers', context.rawEvent.headers)
487
+ // If not defined by other middleware
488
+ // In fullstack forms, the method is spoofed and sent as a hidden field
489
+ .addIf('method', this.getMethod(context.rawEvent))
490
+ .add('queryString', context.rawEvent.queryStringParameters)
491
+ .add('protocol', this.getProtocol(context.rawEvent, proxyOptions))
492
+ .add('cookies', CookieCollection.create(context.rawEvent.headers.cookie, cookieOptions, this.getCookieSecret()))
493
+ .add('ip', proxyAddr(this.toNodeMessage(context.rawEvent), isIpTrusted(proxyOptions.trustedIp, proxyOptions.untrustedIp)));
494
+ return await next(context);
495
+ }
496
+ /**
497
+ * Create the IncomingEventSource from the context.
498
+ *
499
+ * @param context - The adapter context containing the raw event, execution context, and other data.
500
+ * @returns The Incoming Event Source.
501
+ */
502
+ getSource(context) {
503
+ return {
504
+ rawEvent: context.rawEvent,
505
+ platform: AWS_LAMBDA_HTTP_PLATFORM,
506
+ rawContext: context.executionContext
507
+ };
508
+ }
509
+ /**
510
+ * Extracts the HTTP method from the incoming rawEvent.
511
+ *
512
+ * @param rawEvent - The incoming rawEvent.
513
+ * @returns The HTTP method string.
514
+ */
515
+ getMethod(rawEvent) {
516
+ return rawEvent.httpMethod ??
517
+ rawEvent.requestContext?.httpMethod ??
518
+ rawEvent.requestContext?.http?.method ??
519
+ 'GET';
520
+ }
521
+ /**
522
+ * Extracts proxy-related options from the blueprint.
523
+ *
524
+ * @returns Proxy options.
525
+ */
526
+ getProxyOptions() {
527
+ const defaultProxyOptions = { trusted: [], trustedIp: [], untrustedIp: [] };
528
+ const proxyOptions = this.blueprint.get('stone.http.proxies', defaultProxyOptions);
529
+ proxyOptions.trusted = this.blueprint.get('stone.http.hosts.trusted', []);
530
+ return proxyOptions;
531
+ }
532
+ /**
533
+ * Retrieves cookie-related options from the blueprint.
534
+ *
535
+ * @returns Cookie options.
536
+ */
537
+ getCookieOptions() {
538
+ return this.blueprint.get('stone.http.cookie.options', {});
539
+ }
540
+ /**
541
+ * Retrieves the cookie secret from the blueprint.
542
+ *
543
+ * @returns The cookie secret string.
544
+ */
545
+ getCookieSecret() {
546
+ return this.blueprint.get('stone.http.cookie.secret', this.blueprint.get('stone.secret', ''));
547
+ }
548
+ /**
549
+ * Extracts and parses the URL from the incoming rawEvent.
550
+ *
551
+ * @param rawEvent - The incoming HTTP rawEvent.
552
+ * @param options - Proxy options.
553
+ * @returns The parsed URL object.
554
+ */
555
+ extractUrl(rawEvent, options) {
556
+ const hostname = getHostname(this.getRemoteAddress(rawEvent), rawEvent.headers, options);
557
+ const proto = getProtocol(this.getRemoteAddress(rawEvent), rawEvent.headers, true, options);
558
+ return new URL(rawEvent.path ?? rawEvent.rawPath ?? '', `${String(proto)}://${String(hostname)}`);
559
+ }
560
+ /**
561
+ * Extracts a list of IP addresses from the incoming rawEvent.
562
+ *
563
+ * @param rawEvent - The incoming HTTP rawEvent.
564
+ * @param options - Proxy options.
565
+ * @returns An array of IP addresses.
566
+ */
567
+ extractIpAddresses(rawEvent, options) {
568
+ const isTrusted = isIpTrusted(options.trustedIp, options.untrustedIp);
569
+ return proxyAddr.all(this.toNodeMessage(rawEvent), isTrusted).slice(1).reverse();
570
+ }
571
+ /**
572
+ * Converts the incoming rawEvent to a Node.js IncomingMessage.
573
+ *
574
+ * @param rawEvent - The incoming rawEvent.
575
+ * @returns The converted IncomingMessage.
576
+ */
577
+ toNodeMessage(rawEvent) {
578
+ return {
579
+ connection: { remoteAddress: this.getRemoteAddress(rawEvent) },
580
+ headers: { 'x-forwarded-for': rawEvent.headers['x-forwarded-for'] ?? rawEvent.headers['X-Forwarded-For'] }
581
+ };
582
+ }
583
+ /**
584
+ * Determines the protocol from the incoming rawEvent.
585
+ *
586
+ * @param rawEvent - The incoming rawEvent.
587
+ * @param options - Proxy options.
588
+ * @returns The protocol string.
589
+ */
590
+ getProtocol(rawEvent, options) {
591
+ return getProtocol(this.getRemoteAddress(rawEvent), rawEvent.headers, true, options);
592
+ }
593
+ /**
594
+ * Retrieves the remote address from the incoming rawEvent.
595
+ * This method is used as a fallback when the remote address is not found in the rawEvent.
596
+ * @param rawEvent - The incoming rawEvent.
597
+ * @returns The remote address string.
598
+ */
599
+ getRemoteAddress(rawEvent) {
600
+ return rawEvent.requestContext?.http?.sourceIp ?? rawEvent.requestContext?.identity?.sourceIp ?? '';
601
+ }
602
+ }
603
+ /**
604
+ * Meta Middleware for processing incoming events.
605
+ */
606
+ const MetaIncomingEventMiddleware = { module: IncomingEventMiddleware, isClass: true };
607
+
608
+ /**
609
+ * Middleware for handling server responses and transforming them into the appropriate HTTP responses.
610
+ *
611
+ * This middleware processes outgoing responses and attaches the necessary headers, status codes,
612
+ * and body content to the HTTP response.
613
+ */
614
+ class ServerResponseMiddleware {
615
+ /**
616
+ * Handles the outgoing response, processes it, and invokes the next middleware in the pipeline.
617
+ *
618
+ * @param context - The adapter context containing the raw event, execution context, and other data.
619
+ * @param next - The next middleware to be invoked in the pipeline.
620
+ * @returns A promise resolving to the rawResponseBuilder.
621
+ * @throws {AwsLambdaHttpAdapterError} If required components are missing in the context.
622
+ */
623
+ async handle(context, next) {
624
+ const rawResponseBuilder = await next(context);
625
+ if (context.rawEvent === undefined || context.incomingEvent === undefined || context.outgoingResponse === undefined || rawResponseBuilder?.add === undefined) {
626
+ throw new AwsLambdaHttpAdapterError('The context is missing required components.');
627
+ }
628
+ rawResponseBuilder
629
+ .add('headers', context.outgoingResponse.headers)
630
+ .add('statusCode', context.outgoingResponse.statusCode ?? 500)
631
+ .add('statusMessage', context.outgoingResponse.statusMessage ?? statuses.message[context.outgoingResponse.statusCode ?? 500]);
632
+ if (!context.incomingEvent.isMethod('HEAD')) {
633
+ if (context.outgoingResponse instanceof BinaryFileResponse) {
634
+ rawResponseBuilder
635
+ .add('isBase64Encoded', true)
636
+ .add('body', context.outgoingResponse.file.getContent('base64'));
637
+ }
638
+ else {
639
+ const isBuffer = Buffer.isBuffer(context.outgoingResponse.content);
640
+ const content = isBuffer
641
+ ? context.outgoingResponse.content.toString('base64')
642
+ : context.outgoingResponse.content;
643
+ rawResponseBuilder
644
+ .add('body', content)
645
+ .add('isBase64Encoded', isBuffer)
646
+ .add('charset', context.outgoingResponse.charset);
647
+ }
648
+ }
649
+ return rawResponseBuilder;
650
+ }
651
+ }
652
+ /**
653
+ * Meta Middleware for processing server responses.
654
+ */
655
+ const MetaServerResponseMiddleware = { module: ServerResponseMiddleware, isClass: true };
656
+
657
+ /**
658
+ * Default blueprint configuration for the AWS Lambda Http Adapter.
659
+ *
660
+ * This blueprint defines the initial configuration for the AWS Lambda Http adapter
661
+ * within the Stone.js framework. It includes:
662
+ * - An alias for the AWS Lambda platform (`AWS_LAMBDA_HTTP_PLATFORM`).
663
+ * - A default resolver function (currently a placeholder).
664
+ * - Middleware, hooks, and state flags (`current`, `default`, `preferred`).
665
+ */
666
+ const awsLambdaHttpAdapterBlueprint = {
667
+ stone: {
668
+ ...httpCoreBlueprint.stone,
669
+ blueprint: {
670
+ middleware: metaAdapterBlueprintMiddleware
671
+ },
672
+ adapters: [
673
+ {
674
+ current: false,
675
+ variant: 'server',
676
+ platform: AWS_LAMBDA_HTTP_PLATFORM,
677
+ middleware: [
678
+ MetaIncomingEventMiddleware,
679
+ MetaServerResponseMiddleware
680
+ ],
681
+ resolver: awsLambdaHttpAdapterResolver,
682
+ eventHandlerResolver: defaultKernelResolver,
683
+ errorHandlers: {
684
+ default: { module: AwsLambdaHttpErrorHandler, isClass: true }
685
+ },
686
+ default: isNotEmpty(getString('AWS_LAMBDA_FUNCTION_NAME', ''))
687
+ }
688
+ ]
689
+ }
690
+ };
691
+
692
+ /**
693
+ * A Stone.js decorator that integrates the AWS Lambda HTTP Adapter with a class.
694
+ *
695
+ * This decorator modifies the class to seamlessly enable AWS Lambda HTTP as the
696
+ * execution environment for a Stone.js application. By applying this decorator,
697
+ * the class is automatically configured with the necessary blueprint for AWS Lambda HTTP.
698
+ *
699
+ * @template T - The type of the class being decorated. Defaults to `ClassType`.
700
+ * @param options - Optional configuration to customize the AWS Lambda HTTP Adapter.
701
+ *
702
+ * @returns A class decorator that applies the AWS Lambda HTTP adapter configuration.
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * import { AwsLambdaHttp } from '@stone-js/aws-lambda-http-adapter';
707
+ *
708
+ * @AwsLambdaHttp({
709
+ * alias: 'MyAwsLambdaHttpAdapter',
710
+ * current: true,
711
+ * })
712
+ * class App {
713
+ * // Your application logic here
714
+ * }
715
+ * ```
716
+ */
717
+ const AwsLambdaHttp = (options = {}) => {
718
+ return classDecoratorLegacyWrapper((target, context) => {
719
+ if (awsLambdaHttpAdapterBlueprint.stone?.adapters?.[0] !== undefined) {
720
+ // Merge provided options with the default AWS Lambda HTTP adapter blueprint.
721
+ awsLambdaHttpAdapterBlueprint.stone.adapters[0] = deepmerge(awsLambdaHttpAdapterBlueprint.stone.adapters[0], options);
722
+ }
723
+ // Add the modified blueprint to the target class.
724
+ addBlueprint(target, context, awsLambdaHttpAdapterBlueprint);
725
+ });
726
+ };
727
+
728
+ /**
729
+ * Class representing a BodyEventMiddleware.
730
+ *
731
+ * This middleware handles platform-specific messages and transforms them into Stone.js IncomingEvent objects.
732
+ *
733
+ * @author Mr. Stone
734
+ */
735
+ class BodyEventMiddleware {
736
+ /**
737
+ * The blueprint for resolving configuration and dependencies.
738
+ */
739
+ blueprint;
740
+ /**
741
+ * Create a BodyEventMiddleware.
742
+ *
743
+ * @param {blueprint} options - Options for creating the BodyEventMiddleware.
744
+ */
745
+ constructor({ blueprint }) {
746
+ this.blueprint = blueprint;
747
+ }
748
+ /**
749
+ * Handles the incoming event, processes it, and invokes the next middleware in the pipeline.
750
+ *
751
+ * @param context - The adapter context containing the raw event, execution context, and other data.
752
+ * @param next - The next middleware to be invoked in the pipeline.
753
+ * @returns A promise that resolves to the destination type after processing.
754
+ *
755
+ * @throws {AwsLambdaHttpAdapterError} If required components such as the rawEvent or IncomingEventBuilder are not provided.
756
+ */
757
+ async handle(context, next) {
758
+ if (context.rawEvent === undefined || context.incomingEventBuilder?.add === undefined) {
759
+ throw new AwsLambdaHttpAdapterError('The context is missing required components.');
760
+ }
761
+ if (!isMultipart(this.toNodeMessage(context.rawEvent))) {
762
+ const body = this.getBody(this.toNodeMessage(context.rawEvent), context.rawEvent);
763
+ const method = body.$method$;
764
+ context
765
+ .incomingEventBuilder
766
+ .add('body', body)
767
+ .add('metadata', body);
768
+ // In fullstack forms, the method is spoofed and sent as a hidden field
769
+ isNotEmpty(method) && context.incomingEventBuilder.add('method', method);
770
+ }
771
+ return await next(context);
772
+ }
773
+ /**
774
+ * Convert the raw event into a Node.js IncomingMessage.
775
+ *
776
+ * @param rawEvent - The raw event from the platform.
777
+ * @returns The converted IncomingMessage.
778
+ */
779
+ toNodeMessage(rawEvent) {
780
+ return {
781
+ headers: {
782
+ 'content-type': rawEvent.headers['content-type'] ?? rawEvent.headers['Content-Type'],
783
+ 'content-length': rawEvent.headers['content-length'] ?? rawEvent.headers['Content-Length'],
784
+ 'transfer-encoding': rawEvent.headers['transfer-encoding'] ?? rawEvent.headers['Transfer-Encoding']
785
+ }
786
+ };
787
+ }
788
+ /**
789
+ * Extract and parse the body from the message.
790
+ *
791
+ * @param message - The incoming HTTP message.
792
+ * @returns A Promise resolving to the parsed body.
793
+ * @throws {AwsLambdaHttpAdapterError} If the body parsing fails or is invalid.
794
+ */
795
+ getBody(message, rawEvent) {
796
+ if (!typeIs.hasBody(message)) {
797
+ return {};
798
+ }
799
+ const defaultOptions = { limit: '100kb', defaultType: 'text/plain', defaultCharset: 'utf-8' };
800
+ const { defaultType, defaultCharset, limit: rawLimit } = this.blueprint.get('stone.http.body', defaultOptions);
801
+ const limit = bytes.parse(rawLimit) ?? 100000;
802
+ const encoding = getCharset(message, defaultCharset);
803
+ const type = getType(message, defaultType);
804
+ if (typeIs.is(type, ['urlencoded', 'json', 'text', 'bin']) === false) {
805
+ return {};
806
+ }
807
+ const stringifiedBody = typeof rawEvent.body === 'string'
808
+ ? rawEvent.body
809
+ : (typeof rawEvent.body === 'object' && rawEvent.body !== null
810
+ ? JSON.stringify(rawEvent.body)
811
+ : '');
812
+ if (Buffer.byteLength(stringifiedBody, encoding) > limit) {
813
+ throw new AwsLambdaHttpAdapterError('The context is missing required components.');
814
+ }
815
+ return rawEvent.body;
816
+ }
817
+ }
818
+ /**
819
+ * Meta Middleware for processing the request body.
820
+ */
821
+ const MetaBodyEventMiddleware = { module: BodyEventMiddleware, isClass: true };
822
+
823
+ /**
824
+ * Class representing a FilesEventMiddleware.
825
+ *
826
+ * @author Mr. Stone <evensstone@gmail.com>
827
+ */
828
+ class FilesEventMiddleware {
829
+ /**
830
+ * The blueprint for resolving configuration and dependencies.
831
+ */
832
+ blueprint;
833
+ /**
834
+ * Create a FilesEventMiddleware.
835
+ *
836
+ * @param {blueprint} options - Options for creating the FilesEventMiddleware.
837
+ */
838
+ constructor({ blueprint }) {
839
+ this.blueprint = blueprint;
840
+ }
841
+ /**
842
+ * Handles the incoming event, processes it, and invokes the next middleware in the pipeline.
843
+ *
844
+ * @param context - The adapter context containing the raw event, execution context, and other data.
845
+ * @param next - The next middleware to be invoked in the pipeline.
846
+ * @returns A promise that resolves to the destination type after processing.
847
+ *
848
+ * @throws {AwsLambdaHttpAdapterError} If required components such as the rawEvent or IncomingEventBuilder are not provided.
849
+ */
850
+ async handle(context, next) {
851
+ if (context.rawEvent === undefined || context.incomingEventBuilder?.add === undefined) {
852
+ throw new AwsLambdaHttpAdapterError('The context is missing required components.');
853
+ }
854
+ if (isMultipart(this.normalizeEvent(context.rawEvent))) {
855
+ const options = this.blueprint.get('stone.http.files.upload', {});
856
+ const response = await getFilesUploads(this.normalizeEvent(context.rawEvent), options);
857
+ const method = response.fields.$method$;
858
+ context
859
+ .incomingEventBuilder
860
+ .add('files', response.files)
861
+ .add('body', response.fields);
862
+ // In fullstack forms, the method is spoofed and sent as a hidden field
863
+ isNotEmpty(method) && context.incomingEventBuilder.add('method', method);
864
+ }
865
+ return await next(context);
866
+ }
867
+ /**
868
+ * Normalize the incoming event to an IncomingMessage.
869
+ *
870
+ * @param rawEvent - The raw event to be normalized.
871
+ * @returns The normalized event.
872
+ */
873
+ normalizeEvent(rawEvent) {
874
+ return {
875
+ body: rawEvent.body,
876
+ headers: {
877
+ 'content-type': rawEvent.headers['content-type'] ?? rawEvent.headers['Content-Type'],
878
+ 'content-length': rawEvent.headers['content-length'] ?? rawEvent.headers['Content-Length'],
879
+ 'transfer-encoding': rawEvent.headers['transfer-encoding'] ?? rawEvent.headers['Transfer-Encoding']
880
+ }
881
+ };
882
+ }
883
+ }
884
+ /**
885
+ * Meta Middleware for processing files uploads.
886
+ */
887
+ const MetaFilesEventMiddleware = { module: FilesEventMiddleware, isClass: true };
888
+
889
+ export { AWS_LAMBDA_HTTP_PLATFORM, AwsLambdaHttp, AwsLambdaHttpAdapter, AwsLambdaHttpAdapterError, AwsLambdaHttpErrorHandler, BodyEventMiddleware, FilesEventMiddleware, IncomingEventMiddleware, MetaBodyEventMiddleware, MetaFilesEventMiddleware, MetaIncomingEventMiddleware, MetaServerResponseMiddleware, RawHttpResponseWrapper, ServerResponseMiddleware, SetAwsLambdaHttpResponseResolverMiddleware, awsLambdaHttpAdapterBlueprint, awsLambdaHttpAdapterResolver, metaAdapterBlueprintMiddleware };