@bitblit/ratchet-epsilon-common 6.0.146-alpha → 6.0.148-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/lib/config/cron/abstract-cron-entry.d.ts +1 -1
  2. package/package.json +11 -10
  3. package/src/background/background-dynamo-log-table-handler.ts +44 -0
  4. package/src/background/background-entry.ts +4 -0
  5. package/src/background/background-execution-event-type.ts +9 -0
  6. package/src/background/background-execution-event.ts +9 -0
  7. package/src/background/background-execution-listener.ts +6 -0
  8. package/src/background/background-handler.ts +352 -0
  9. package/src/background/background-http-adapter-handler.ts +166 -0
  10. package/src/background/background-meta-response-internal.ts +5 -0
  11. package/src/background/background-process-handling.ts +6 -0
  12. package/src/background/background-process-log-table-entry.ts +11 -0
  13. package/src/background/background-queue-response-internal.ts +9 -0
  14. package/src/background/background-validator.ts +105 -0
  15. package/src/background/epsilon-background-process-error.ts +110 -0
  16. package/src/background/internal-background-entry.ts +10 -0
  17. package/src/background/manager/abstract-background-manager.ts +120 -0
  18. package/src/background/manager/aws-large-payload-s3-sqs-sns-background-manager.ts +87 -0
  19. package/src/background/manager/aws-sqs-sns-background-manager.ts +201 -0
  20. package/src/background/manager/background-manager-like.ts +44 -0
  21. package/src/background/manager/background-manager.spec.ts +89 -0
  22. package/src/background/manager/single-thread-local-background-manager.ts +58 -0
  23. package/src/background/s3-background-transaction-logger.ts +65 -0
  24. package/src/build/ratchet-epsilon-common-info.ts +19 -0
  25. package/src/built-in/background/echo-processor.ts +17 -0
  26. package/src/built-in/background/log-and-enqueue-echo-processor.ts +14 -0
  27. package/src/built-in/background/log-message-background-error-processor.ts +10 -0
  28. package/src/built-in/background/no-op-processor.ts +12 -0
  29. package/src/built-in/background/retry-processor.ts +51 -0
  30. package/src/built-in/background/sample-delay-processor.ts +15 -0
  31. package/src/built-in/background/sample-input-validated-processor-data.ts +4 -0
  32. package/src/built-in/background/sample-input-validated-processor.ts +14 -0
  33. package/src/built-in/built-in-trace-id-generators.ts +22 -0
  34. package/src/built-in/daemon/daemon-authorizer-function.ts +4 -0
  35. package/src/built-in/daemon/daemon-config.ts +9 -0
  36. package/src/built-in/daemon/daemon-group-selection-function.ts +3 -0
  37. package/src/built-in/daemon/daemon-handler.ts +87 -0
  38. package/src/built-in/daemon/daemon-process-state-list.ts +9 -0
  39. package/src/built-in/http/apollo/apollo-util.ts +43 -0
  40. package/src/built-in/http/apollo/default-epsilon-apollo-context.ts +11 -0
  41. package/src/built-in/http/apollo/epsilon-apollo-context-builder-options.ts +5 -0
  42. package/src/built-in/http/apollo/epsilon-lambda-apollo-context-function-argument.ts +6 -0
  43. package/src/built-in/http/apollo/epsilon-lambda-apollo-options.ts +11 -0
  44. package/src/built-in/http/apollo-filter.ts +151 -0
  45. package/src/built-in/http/built-in-auth-filters.ts +73 -0
  46. package/src/built-in/http/built-in-authorizers.ts +22 -0
  47. package/src/built-in/http/built-in-filters.spec.ts +26 -0
  48. package/src/built-in/http/built-in-filters.ts +300 -0
  49. package/src/built-in/http/built-in-handlers.ts +85 -0
  50. package/src/built-in/http/log-level-manipulation-filter.ts +26 -0
  51. package/src/built-in/http/run-handler-as-filter.spec.ts +67 -0
  52. package/src/built-in/http/run-handler-as-filter.ts +102 -0
  53. package/src/cli/ratchet-cli-handler.ts +23 -0
  54. package/src/cli/run-background-process-from-command-line.ts +32 -0
  55. package/src/config/background/background-aws-config.ts +8 -0
  56. package/src/config/background/background-config.ts +15 -0
  57. package/src/config/background/background-error-processor.ts +5 -0
  58. package/src/config/background/background-processor.ts +14 -0
  59. package/src/config/background/background-transaction-log.ts +9 -0
  60. package/src/config/background/background-transaction-logger.ts +6 -0
  61. package/src/config/cron/abstract-cron-entry.ts +17 -0
  62. package/src/config/cron/cron-background-entry.ts +17 -0
  63. package/src/config/cron/cron-config.ts +10 -0
  64. package/src/config/dynamo-db-config.ts +6 -0
  65. package/src/config/epsilon-config.ts +30 -0
  66. package/src/config/epsilon-lambda-event-handler.ts +12 -0
  67. package/src/config/epsilon-logger-config.ts +23 -0
  68. package/src/config/espilon-server-mode.ts +10 -0
  69. package/src/config/generic-aws-event-handler-function.ts +1 -0
  70. package/src/config/http/authorizer-function.ts +9 -0
  71. package/src/config/http/epsilon-authorization-context.ts +5 -0
  72. package/src/config/http/epsilon-cors-approach.ts +7 -0
  73. package/src/config/http/extended-api-gateway-event.ts +8 -0
  74. package/src/config/http/filter-chain-context.ts +15 -0
  75. package/src/config/http/filter-function.ts +3 -0
  76. package/src/config/http/handler-function.ts +4 -0
  77. package/src/config/http/http-config.ts +27 -0
  78. package/src/config/http/http-processing-config.ts +23 -0
  79. package/src/config/http/mapped-http-processing-config.ts +12 -0
  80. package/src/config/http/null-returned-object-handling.ts +7 -0
  81. package/src/config/inter-api/inter-api-aws-config.ts +5 -0
  82. package/src/config/inter-api/inter-api-config.ts +7 -0
  83. package/src/config/inter-api/inter-api-process-mapping.ts +11 -0
  84. package/src/config/local-server/local-server-event-logging-style.ts +8 -0
  85. package/src/config/local-server/local-server-http-method-handling.ts +7 -0
  86. package/src/config/local-server/local-server-options.ts +12 -0
  87. package/src/config/logging-trace-id-generator.ts +3 -0
  88. package/src/config/no-handlers-found-error.ts +6 -0
  89. package/src/config/open-api/open-api-document-components.ts +4 -0
  90. package/src/config/open-api/open-api-document.ts +7 -0
  91. package/src/config/s3-config.ts +8 -0
  92. package/src/config/sns-config.ts +7 -0
  93. package/src/config/sqs-config.ts +7 -0
  94. package/src/epsilon-build-properties.ts +21 -0
  95. package/src/epsilon-constants.ts +62 -0
  96. package/src/epsilon-global-handler.ts +238 -0
  97. package/src/epsilon-instance.ts +20 -0
  98. package/src/epsilon-logging-extension-processor.ts +19 -0
  99. package/src/http/auth/api-gateway-adapter-authentication-handler.ts +95 -0
  100. package/src/http/auth/auth0-web-token-manipulator.ts +69 -0
  101. package/src/http/auth/basic-auth-token.ts +7 -0
  102. package/src/http/auth/google-web-token-manipulator.spec.ts +15 -0
  103. package/src/http/auth/google-web-token-manipulator.ts +80 -0
  104. package/src/http/auth/jwt-ratchet-local-web-token-manipulator.ts +37 -0
  105. package/src/http/auth/local-web-token-manipulator.spec.ts +34 -0
  106. package/src/http/auth/local-web-token-manipulator.ts +114 -0
  107. package/src/http/auth/web-token-manipulator.ts +9 -0
  108. package/src/http/error/bad-gateway.ts +11 -0
  109. package/src/http/error/bad-request-error.ts +11 -0
  110. package/src/http/error/conflict-error.ts +12 -0
  111. package/src/http/error/forbidden-error.ts +12 -0
  112. package/src/http/error/gateway-timeout.ts +12 -0
  113. package/src/http/error/method-not-allowed-error.ts +12 -0
  114. package/src/http/error/misconfigured-error.ts +12 -0
  115. package/src/http/error/not-found-error.ts +12 -0
  116. package/src/http/error/not-implemented.ts +12 -0
  117. package/src/http/error/request-timeout-error.ts +12 -0
  118. package/src/http/error/service-unavailable.ts +12 -0
  119. package/src/http/error/too-many-requests-error.ts +12 -0
  120. package/src/http/error/unauthorized-error.ts +12 -0
  121. package/src/http/event-util.spec.ts +190 -0
  122. package/src/http/event-util.ts +272 -0
  123. package/src/http/response-util.spec.ts +117 -0
  124. package/src/http/response-util.ts +164 -0
  125. package/src/http/route/epsilon-router.ts +9 -0
  126. package/src/http/route/extended-auth-response-context.ts +7 -0
  127. package/src/http/route/route-and-parse.ts +8 -0
  128. package/src/http/route/route-mapping.ts +21 -0
  129. package/src/http/route/route-validator-config.ts +5 -0
  130. package/src/http/route/router-util.spec.ts +33 -0
  131. package/src/http/route/router-util.ts +314 -0
  132. package/src/http/web-handler.spec.ts +99 -0
  133. package/src/http/web-handler.ts +157 -0
  134. package/src/http/web-v2-handler.ts +34 -0
  135. package/src/inter-api/inter-api-entry.ts +8 -0
  136. package/src/inter-api/inter-api-util.spec.ts +77 -0
  137. package/src/inter-api/inter-api-util.ts +71 -0
  138. package/src/inter-api-manager.ts +75 -0
  139. package/src/lambda-event-handler/cron-epsilon-lambda-event-handler.spec.ts +130 -0
  140. package/src/lambda-event-handler/cron-epsilon-lambda-event-handler.ts +132 -0
  141. package/src/lambda-event-handler/dynamo-epsilon-lambda-event-handler.ts +42 -0
  142. package/src/lambda-event-handler/generic-sns-epsilon-lambda-event-handler.ts +38 -0
  143. package/src/lambda-event-handler/generic-sqs-epsilon-lambda-event-handler.ts +43 -0
  144. package/src/lambda-event-handler/inter-api-epsilon-lambda-event-handler.ts +33 -0
  145. package/src/lambda-event-handler/s3-epsilon-lambda-event-handler.ts +50 -0
  146. package/src/local-container-server.ts +128 -0
  147. package/src/local-server.spec.ts +16 -0
  148. package/src/local-server.ts +426 -0
  149. package/src/open-api-util/open-api-doc-modifications.ts +9 -0
  150. package/src/open-api-util/open-api-doc-modifier.spec.ts +22 -0
  151. package/src/open-api-util/open-api-doc-modifier.ts +90 -0
  152. package/src/open-api-util/yaml-combiner.spec.ts +26 -0
  153. package/src/open-api-util/yaml-combiner.ts +35 -0
  154. package/src/sample/sample-server-components-with-apollo.ts +87 -0
  155. package/src/sample/sample-server-components.ts +183 -0
  156. package/src/sample/sample-server-static-files.ts +614 -0
  157. package/src/sample/test-error-server.ts +140 -0
  158. package/src/util/aws-util.ts +89 -0
  159. package/src/util/context-global-data.ts +13 -0
  160. package/src/util/context-util.ts +156 -0
  161. package/src/util/cron-util.spec.ts +190 -0
  162. package/src/util/cron-util.ts +86 -0
  163. package/src/util/epsilon-config-parser.ts +90 -0
  164. package/src/util/epsilon-server-util.spec.ts +18 -0
  165. package/src/util/epsilon-server-util.ts +16 -0
@@ -0,0 +1,11 @@
1
+ import { BaseContext, ContextFunction } from '@apollo/server';
2
+ import { EpsilonLambdaApolloContextFunctionArgument } from './epsilon-lambda-apollo-context-function-argument.js';
3
+ import { ProxyResult } from 'aws-lambda';
4
+ import { EpsilonCorsApproach } from '../../../config/http/epsilon-cors-approach.js';
5
+
6
+ export interface EpsilonLambdaApolloOptions<TContext extends BaseContext> {
7
+ context?: ContextFunction<[EpsilonLambdaApolloContextFunctionArgument], TContext>;
8
+ timeoutMS?: number; // Max time to wait for apollo
9
+ corsMethod?: EpsilonCorsApproach;
10
+ debugOutputCallback?: (resp: ProxyResult) => Promise<void>;
11
+ }
@@ -0,0 +1,151 @@
1
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
2
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
3
+ import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
4
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
5
+ import { TimeoutToken } from '@bitblit/ratchet-common/lang/timeout-token';
6
+ import { RestfulApiHttpError } from '@bitblit/ratchet-common/network/restful-api-http-error';
7
+ import { Base64Ratchet } from '@bitblit/ratchet-common/lang/base64-ratchet';
8
+ import { APIGatewayEvent, Context, ProxyResult } from 'aws-lambda';
9
+ import { RequestTimeoutError } from '../../http/error/request-timeout-error.js';
10
+ import { FilterFunction } from '../../config/http/filter-function.js';
11
+ import { FilterChainContext } from '../../config/http/filter-chain-context.js';
12
+ import { ApolloServer, BaseContext, ContextFunction, HeaderMap, HTTPGraphQLRequest, HTTPGraphQLResponse } from '@apollo/server';
13
+ import { ContextUtil } from '../../util/context-util.js';
14
+ import { EpsilonLambdaApolloOptions } from './apollo/epsilon-lambda-apollo-options.js';
15
+ import { EpsilonLambdaApolloContextFunctionArgument } from './apollo/epsilon-lambda-apollo-context-function-argument.js';
16
+ import { ApolloUtil } from './apollo/apollo-util.js';
17
+ import { BuiltInFilters } from './built-in-filters.js';
18
+
19
+ export class ApolloFilter {
20
+ public static async handlePathWithApollo<T>(
21
+ fCtx: FilterChainContext,
22
+ apolloPathRegex: RegExp,
23
+ apolloServer: ApolloServer<T>,
24
+ options?: EpsilonLambdaApolloOptions<T>,
25
+ ): Promise<boolean> {
26
+ let rval: boolean = false;
27
+
28
+ if (fCtx.event?.path && apolloPathRegex && apolloPathRegex.test(fCtx.event.path)) {
29
+ fCtx.result = await ApolloFilter.processApolloRequest(fCtx.event, fCtx.context, apolloServer, options);
30
+
31
+ if (options?.corsMethod) {
32
+ await BuiltInFilters.addCorsHeadersDynamically(fCtx, options.corsMethod);
33
+ }
34
+ } else {
35
+ // Not handled by apollo
36
+ rval = true;
37
+ }
38
+
39
+ return rval;
40
+ }
41
+
42
+ public static async processApolloRequest<T>(
43
+ event: APIGatewayEvent,
44
+ context: Context,
45
+ apolloServer: ApolloServer<T>,
46
+ options?: EpsilonLambdaApolloOptions<T>,
47
+ ): Promise<ProxyResult> {
48
+ Logger.silly('Processing event with apollo: %j', event);
49
+ let rval: ProxyResult = null;
50
+ RequireRatchet.notNullOrUndefined(apolloServer, 'apolloServer');
51
+ apolloServer.assertStarted('Cannot process with apollo - instance not started');
52
+
53
+ const headerMap: HeaderMap = new HeaderMap();
54
+ for (const headersKey in event.headers) {
55
+ headerMap.set(headersKey, event.headers[headersKey]);
56
+ }
57
+
58
+ const eventMethod: string = StringRatchet.trimToEmpty(event.httpMethod).toUpperCase();
59
+ let body: any = null;
60
+ if (StringRatchet.trimToNull(event.body)) {
61
+ const bodyString: string = event.isBase64Encoded ? Base64Ratchet.base64StringToString(event.body) : event.body;
62
+ body = JSON.parse(bodyString);
63
+ }
64
+
65
+ const aRequest: HTTPGraphQLRequest = {
66
+ method: eventMethod,
67
+ headers: headerMap,
68
+ search:
69
+ eventMethod === 'GET' && event?.queryStringParameters
70
+ ? Object.keys(event.queryStringParameters)
71
+ .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(event.queryStringParameters[k]))
72
+ .join('&')
73
+ : null,
74
+ body: body,
75
+ };
76
+
77
+ // We do this because fully timing out on Lambda is never a good thing
78
+ const timeoutMS: number = options?.timeoutMS ?? context.getRemainingTimeInMillis() - 500;
79
+
80
+ //const defaultContextFn: ContextFunction<[EpsilonLambdaApolloContextFunctionArgument], any> = async () => ({});
81
+
82
+ const contextFn: ContextFunction<[EpsilonLambdaApolloContextFunctionArgument], T> = options?.context ?? ApolloUtil.emptyContext;
83
+
84
+ const apolloPromise = apolloServer.executeHTTPGraphQLRequest({
85
+ httpGraphQLRequest: aRequest,
86
+ context: () => contextFn({ lambdaContext: context, lambdaEvent: event }),
87
+ });
88
+
89
+ let result: HTTPGraphQLResponse | TimeoutToken = null;
90
+ if (timeoutMS) {
91
+ result = await PromiseRatchet.timeout(apolloPromise, 'Apollo timed out after ' + timeoutMS + ' ms.', timeoutMS);
92
+ } else {
93
+ Logger.warn('No timeout set even after defaulting for Apollo');
94
+ result = await apolloPromise;
95
+ }
96
+
97
+ if (TimeoutToken.isTimeoutToken(result)) {
98
+ (result as TimeoutToken).writeToLog();
99
+ throw new RequestTimeoutError('Timed out');
100
+ }
101
+
102
+ // If we reach here we didn't time out
103
+ const httpGraphQLResponse: HTTPGraphQLResponse = result as HTTPGraphQLResponse; // TODO: Use typeguard here instead
104
+
105
+ const outHeaders: Record<string, string> = {};
106
+
107
+ for (const [headersKey, headersValue] of httpGraphQLResponse.headers.entries()) {
108
+ outHeaders[headersKey] = headersValue;
109
+ }
110
+
111
+ if (httpGraphQLResponse.body.kind === 'chunked') {
112
+ // This is legal according to https://www.apollographql.com/docs/apollo-server/integrations/building-integrations/
113
+ throw new RestfulApiHttpError('Apollo returned chunked result').withHttpStatusCode(500).withRequestId(ContextUtil.currentRequestId());
114
+ }
115
+
116
+ const bodyAsString: string = StringRatchet.trimToEmpty(httpGraphQLResponse?.body?.string);
117
+
118
+ rval = {
119
+ body: Base64Ratchet.generateBase64VersionOfString(bodyAsString),
120
+ headers: outHeaders,
121
+ multiValueHeaders: {}, // TODO: Need setting?
122
+ isBase64Encoded: true,
123
+ statusCode: httpGraphQLResponse.status || 200,
124
+ };
125
+
126
+ // Finally, a double check to set the content type correctly if the browser page was shown
127
+ // Since otherwise Apollo defaults it to application/json for some reason
128
+ if (eventMethod === 'GET' && rval.headers['content-type'] !== 'text/html' && bodyAsString.indexOf('<!DOCTYPE html>') >= 0) {
129
+ Logger.info('Forcing content type to html for the sandbox page');
130
+ rval.headers = rval.headers || {};
131
+ rval.headers['content-type'] = 'text/html';
132
+ }
133
+
134
+ if (options.debugOutputCallback) {
135
+ await options.debugOutputCallback(rval);
136
+ }
137
+
138
+ return rval;
139
+ }
140
+
141
+ public static addApolloFilterToList(
142
+ filters: FilterFunction[],
143
+ apolloPathRegex: RegExp,
144
+ apolloServer: ApolloServer,
145
+ options?: EpsilonLambdaApolloOptions<BaseContext>,
146
+ ): void {
147
+ if (filters) {
148
+ filters.push((fCtx) => ApolloFilter.handlePathWithApollo(fCtx, apolloPathRegex, apolloServer, options));
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,73 @@
1
+ import { UnauthorizedError } from '../../http/error/unauthorized-error.js';
2
+ import { MisconfiguredError } from '../../http/error/misconfigured-error.js';
3
+ import { FilterChainContext } from '../../config/http/filter-chain-context.js';
4
+ import { ForbiddenError } from '../../http/error/forbidden-error.js';
5
+ import { AuthorizerFunction } from '../../config/http/authorizer-function.js';
6
+ import { WebTokenManipulator } from '../../http/auth/web-token-manipulator.js';
7
+ import { EventUtil } from '../../http/event-util.js';
8
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
9
+ import { CommonJwtToken } from '@bitblit/ratchet-common/jwt/common-jwt-token';
10
+ import { JwtTokenBase } from '@bitblit/ratchet-common/jwt/jwt-token-base';
11
+
12
+ export class BuiltInAuthFilters {
13
+
14
+ public static async parseAuthorizationHeader(
15
+ fCtx: FilterChainContext,
16
+ webTokenManipulators: WebTokenManipulator<JwtTokenBase> | WebTokenManipulator<JwtTokenBase>[],
17
+ ): Promise<boolean> {
18
+ if (!fCtx?.event || !webTokenManipulators || (Array.isArray(webTokenManipulators) && !webTokenManipulators.length)) {
19
+ throw new MisconfiguredError('Cannot continue - missing event or encryption');
20
+ } else {
21
+ // We dont throw errors if no token - just just decodes, it DOESNT enforce having tokens
22
+ const tokenString: string = EventUtil.extractBearerTokenFromEvent(fCtx?.event);
23
+ if (!Array.isArray(webTokenManipulators)) {
24
+ webTokenManipulators = [webTokenManipulators];
25
+ }
26
+ for (let i = 0; i < webTokenManipulators.length && !fCtx?.event?.authorization?.auth; i++) {
27
+ const manipulator: WebTokenManipulator<JwtTokenBase> = webTokenManipulators[i];
28
+ try {
29
+ // We include the prefix (like 'bearer') in case the token wants to code more than one type
30
+ const token: JwtTokenBase = await manipulator.extractTokenFromAuthorizationHeader(tokenString);
31
+ fCtx.event.authorization = {
32
+ raw: tokenString,
33
+ auth: token,
34
+ error: null,
35
+ };
36
+ } catch (err) {
37
+ fCtx.event.authorization = {
38
+ raw: tokenString,
39
+ auth: null,
40
+ error: err['message'],
41
+ };
42
+ }
43
+ }
44
+ }
45
+ return true;
46
+ }
47
+
48
+ public static async applyOpenApiAuthorization(fCtx: FilterChainContext): Promise<boolean> {
49
+ // Check if this endpoint requires authorization
50
+ // Use !== true below because commonly it just wont be spec'd
51
+ if (StringRatchet.trimToNull(fCtx?.routeAndParse?.mapping?.authorizerName)) {
52
+ const authorizer: AuthorizerFunction = fCtx?.authenticators?.get(fCtx.routeAndParse.mapping.authorizerName);
53
+ if (authorizer) {
54
+ if (fCtx?.event?.authorization?.auth) {
55
+ const allowed: boolean = await authorizer(fCtx.event.authorization, fCtx.event, fCtx.routeAndParse.mapping);
56
+ if (!allowed) {
57
+ throw new ForbiddenError('You lack privileges to see this endpoint');
58
+ }
59
+ } else {
60
+ throw new UnauthorizedError('You need to supply credentials for this endpoint');
61
+ }
62
+ } else {
63
+ throw new MisconfiguredError().withFormattedErrorMessage(
64
+ 'Authorizer %s requested but not found',
65
+ fCtx.routeAndParse.mapping.authorizerName,
66
+ );
67
+ }
68
+ } else {
69
+ // Do nothing (unauthenticated endpoint)
70
+ }
71
+ return true;
72
+ }
73
+ }
@@ -0,0 +1,22 @@
1
+ import { Logger } from "@bitblit/ratchet-common/logger/logger";
2
+ import { APIGatewayEvent } from "aws-lambda";
3
+ import { EpsilonAuthorizationContext } from "../../config/http/epsilon-authorization-context.js";
4
+
5
+ export class BuiltInAuthorizers {
6
+ public static async simpleNoAuthenticationLogAccess(
7
+ authorizationContext: EpsilonAuthorizationContext<any>,
8
+ evt: APIGatewayEvent,
9
+ ): Promise<boolean> {
10
+ // Just logs the request but does nothing else
11
+ Logger.debug('Auth requested for %s : %j', evt.path, authorizationContext?.auth);
12
+ return true;
13
+ }
14
+
15
+ public static async simpleLoggedInAuth(authorizationContext: EpsilonAuthorizationContext<any>, evt: APIGatewayEvent): Promise<boolean> {
16
+ // Just verifies that there is a valid token in the request
17
+ const rval: boolean = !!authorizationContext?.auth;
18
+ Logger.silly('SimpleLoggedInAuth returning %s for %s', rval, evt.path);
19
+ return rval;
20
+ }
21
+
22
+ }
@@ -0,0 +1,26 @@
1
+ import { BuiltInFilters } from './built-in-filters.js';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { ExtendedAPIGatewayEvent } from '../../config/http/extended-api-gateway-event.js';
4
+ import { FilterChainContext } from '../../config/http/filter-chain-context.js';
5
+
6
+ describe('#uriDecodeQueryParams', function () {
7
+ test('should not URL decode query string parameters', async () => {
8
+ const queryParams: Record<string, string> = {
9
+ test: 'fish+chips',
10
+ test2: 'chicken%2bbeef',
11
+ test3: 'ketchup%20mustard',
12
+ test4: '',
13
+ test5: 'cat=dog',
14
+ };
15
+
16
+ BuiltInFilters.uriDecodeQueryParams({
17
+ event: { queryStringParameters: queryParams } as ExtendedAPIGatewayEvent,
18
+ } as FilterChainContext);
19
+
20
+ expect(queryParams['test']).toBe('fish chips');
21
+ expect(queryParams['test2']).toBe('chicken+beef');
22
+ expect(queryParams['test3']).toBe('ketchup mustard');
23
+ expect(queryParams['test4']).toBe('');
24
+ expect(queryParams['test5']).toBe('cat=dog');
25
+ });
26
+ });
@@ -0,0 +1,300 @@
1
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
2
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
3
+ import { RestfulApiHttpError } from '@bitblit/ratchet-common/network/restful-api-http-error';
4
+ import { MapRatchet } from '@bitblit/ratchet-common/lang/map-ratchet';
5
+ import { EventUtil } from '../../http/event-util.js';
6
+ import { BadRequestError } from '../../http/error/bad-request-error.js';
7
+ import { FilterFunction } from '../../config/http/filter-function.js';
8
+ import { ResponseUtil } from '../../http/response-util.js';
9
+ import { FilterChainContext } from '../../config/http/filter-chain-context.js';
10
+ import { MisconfiguredError } from '../../http/error/misconfigured-error.js';
11
+ import { APIGatewayProxyResult } from 'aws-lambda';
12
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
13
+ import { EpsilonCorsApproach } from '../../config/http/epsilon-cors-approach.js';
14
+
15
+ export class BuiltInFilters {
16
+ public static readonly MAXIMUM_LAMBDA_BODY_SIZE_BYTES: number = 1024 * 1024 * 5 - 1024 * 100; // 5Mb - 100k buffer
17
+
18
+ public static async combineFilters(fCtx: FilterChainContext, filters: FilterFunction[]): Promise<boolean> {
19
+ let cont: boolean = true;
20
+ if (filters && filters.length > 0) {
21
+ for (let i = 0; i < filters.length && cont; i++) {
22
+ cont = await filters[i](fCtx);
23
+ }
24
+ }
25
+ return cont;
26
+ }
27
+
28
+ public static async applyGzipIfPossible(fCtx: FilterChainContext): Promise<boolean> {
29
+ if (fCtx.event?.headers && fCtx.result) {
30
+ const encodingHeader: string =
31
+ fCtx.event && fCtx.event.headers ? MapRatchet.extractValueFromMapIgnoreCase(fCtx.event.headers, 'accept-encoding') : null;
32
+ fCtx.result = await ResponseUtil.applyGzipIfPossible(encodingHeader, fCtx.result);
33
+ }
34
+ return true;
35
+ }
36
+
37
+ public static async addConstantHeaders(fCtx: FilterChainContext, headers: Record<string, string>): Promise<boolean> {
38
+ if (headers && fCtx.result) {
39
+ fCtx.result.headers = Object.assign({}, headers, fCtx.result.headers);
40
+ } else {
41
+ Logger.warn('Could not add headers - either result or headers were missing');
42
+ }
43
+ return true;
44
+ }
45
+
46
+ public static async addAWSRequestIdHeader(fCtx: FilterChainContext, headerName: string = 'X-REQUEST-ID'): Promise<boolean> {
47
+ if (fCtx.result && StringRatchet.trimToNull(headerName) && headerName.startsWith('X-')) {
48
+ fCtx.result.headers = fCtx.result.headers || {};
49
+ fCtx.result.headers[headerName] = fCtx.context?.awsRequestId || 'Request-Id-Missing';
50
+ } else {
51
+ Logger.warn('Could not add request id header - either result or context were missing or name was invalid');
52
+ }
53
+ return true;
54
+ }
55
+
56
+ public static async addAllowEverythingCORSHeaders(fCtx: FilterChainContext): Promise<boolean> {
57
+ return BuiltInFilters.addConstantHeaders(fCtx, {
58
+ 'Access-Control-Allow-Origin': '*',
59
+ 'Access-Control-Allow-Methods': '*',
60
+ 'Access-Control-Allow-Headers': '*',
61
+ });
62
+ }
63
+
64
+ public static async addAllowReflectionCORSHeaders(fCtx: FilterChainContext): Promise<boolean> {
65
+ return BuiltInFilters.addConstantHeaders(fCtx, {
66
+ 'Access-Control-Allow-Origin': MapRatchet.caseInsensitiveAccess<string>(fCtx.event.headers, 'Origin') || '*',
67
+ 'Access-Control-Allow-Methods': MapRatchet.caseInsensitiveAccess<string>(fCtx.event.headers, 'Access-Control-Request-Method') || '*',
68
+ 'Access-Control-Allow-Headers': MapRatchet.caseInsensitiveAccess<string>(fCtx.event.headers, 'Access-Control-Request-Headers') || '*',
69
+ });
70
+ }
71
+
72
+ public static async uriDecodeQueryParams(fCtx: FilterChainContext): Promise<boolean> {
73
+ if (fCtx?.event?.queryStringParameters) {
74
+ Object.keys(fCtx.event.queryStringParameters).forEach((k) => {
75
+ const val: string = fCtx.event.queryStringParameters[k];
76
+ if (val) {
77
+ fCtx.event.queryStringParameters[k] = BuiltInFilters.decodeUriComponentAndReplacePlus(val);
78
+ }
79
+ });
80
+ }
81
+ if (fCtx?.event?.multiValueQueryStringParameters) {
82
+ Object.keys(fCtx.event.multiValueQueryStringParameters).forEach((k) => {
83
+ const val: string[] = fCtx.event.multiValueQueryStringParameters[k];
84
+ if (val && val.length) {
85
+ const cleaned: string[] = val.map((v) => BuiltInFilters.decodeUriComponentAndReplacePlus(v));
86
+ fCtx.event.multiValueQueryStringParameters[k] = cleaned;
87
+ }
88
+ });
89
+ }
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Performs decodeURIComponent on a value after replacing all "+" values with spaces.
95
+ */
96
+ private static decodeUriComponentAndReplacePlus(val: string): string {
97
+ return ResponseUtil.decodeUriComponentAndReplacePlus(val);
98
+ }
99
+
100
+ public static async fixStillEncodedQueryParams(fCtx: FilterChainContext): Promise<boolean> {
101
+ EventUtil.fixStillEncodedQueryParams(fCtx.event);
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Basically used to restrict a server to only running on an internal network (weakly, this isn't
107
+ * ironclad at ALL) examines the hostname header to see if what was requested matches a particular
108
+ * host
109
+ * @param hostnameRegExList
110
+ */
111
+ public static createRestrictServerToHostNamesFilter(hostnameRegExList: RegExp[]): FilterFunction {
112
+ RequireRatchet.notNullUndefinedOrEmptyArray(hostnameRegExList, 'hostnameRegExList');
113
+ return async (fCtx: FilterChainContext) => {
114
+ const hostName: string = StringRatchet.trimToNull(MapRatchet.extractValueFromMapIgnoreCase(fCtx?.event?.headers, 'host'));
115
+ if (!StringRatchet.trimToNull(hostName)) {
116
+ throw new BadRequestError('No host name found in headers : ' + JSON.stringify(fCtx?.event?.headers));
117
+ }
118
+ const hostMatches: boolean = EventUtil.hostMatchesRegexInList(hostName, hostnameRegExList);
119
+ if (!hostMatches) {
120
+ throw new BadRequestError('Host does not match list : ' + hostName + ' :: ' + hostnameRegExList);
121
+ }
122
+ return true;
123
+ };
124
+ }
125
+
126
+ public static async disallowStringNullAsPathParameter(fCtx: FilterChainContext): Promise<boolean> {
127
+ if (fCtx?.event?.pathParameters) {
128
+ Object.keys(fCtx.event.pathParameters).forEach((k) => {
129
+ if ('null' === StringRatchet.trimToEmpty(fCtx.event.pathParameters[k]).toLowerCase()) {
130
+ throw new BadRequestError().withFormattedErrorMessage('Path parameter %s was string -null-', k);
131
+ }
132
+ });
133
+ }
134
+ return true;
135
+ }
136
+
137
+ public static async disallowStringNullAsQueryStringParameter(fCtx: FilterChainContext): Promise<boolean> {
138
+ if (fCtx?.event?.queryStringParameters) {
139
+ Object.keys(fCtx.event.queryStringParameters).forEach((k) => {
140
+ if ('null' === StringRatchet.trimToEmpty(fCtx.event.queryStringParameters[k]).toLowerCase()) {
141
+ throw new BadRequestError().withFormattedErrorMessage('Query parameter %s was string -null-', k);
142
+ }
143
+ });
144
+ }
145
+ return true;
146
+ }
147
+
148
+ public static async ensureEventMaps(fCtx: FilterChainContext): Promise<boolean> {
149
+ fCtx.event.queryStringParameters = fCtx.event.queryStringParameters || {};
150
+ fCtx.event.headers = fCtx.event.headers || {};
151
+ fCtx.event.pathParameters = fCtx.event.pathParameters || {};
152
+ return true;
153
+ }
154
+
155
+ public static async parseJsonBodyToObject(fCtx: FilterChainContext): Promise<boolean> {
156
+ if (fCtx.event?.body) {
157
+ try {
158
+ fCtx.event.parsedBody = EventUtil.jsonBodyToObject(fCtx.event);
159
+ } catch (err) {
160
+ throw new RestfulApiHttpError('Supplied body was not parsable as valid JSON').withHttpStatusCode(400).withWrappedError(err);
161
+ }
162
+ }
163
+ return true;
164
+ }
165
+
166
+ public static async checkMaximumLambdaBodySize(fCtx: FilterChainContext): Promise<boolean> {
167
+ if (fCtx.result?.body && fCtx.result.body.length > BuiltInFilters.MAXIMUM_LAMBDA_BODY_SIZE_BYTES) {
168
+ const delta: number = fCtx.result.body.length - BuiltInFilters.MAXIMUM_LAMBDA_BODY_SIZE_BYTES;
169
+ throw new RestfulApiHttpError(
170
+ 'Response size is ' + fCtx.result.body.length + ' bytes, which is ' + delta + ' bytes too large for this handler',
171
+ ).withHttpStatusCode(500);
172
+ }
173
+ return true;
174
+ }
175
+
176
+ public static async validateInboundBody(fCtx: FilterChainContext): Promise<boolean> {
177
+ if (fCtx?.event?.parsedBody && fCtx.routeAndParse) {
178
+ if (fCtx.routeAndParse.mapping.validation) {
179
+ if (!fCtx.modelValidator) {
180
+ throw new MisconfiguredError('Requested body validation but supplied no validator');
181
+ }
182
+ const errors: string[] = fCtx.modelValidator.validate(
183
+ fCtx.routeAndParse.mapping.validation.modelName,
184
+ fCtx.event.parsedBody,
185
+ fCtx.routeAndParse.mapping.validation.emptyAllowed,
186
+ fCtx.routeAndParse.mapping.validation.extraPropertiesAllowed,
187
+ );
188
+ if (errors.length > 0) {
189
+ Logger.info('Found errors while validating %s object %j', fCtx.routeAndParse.mapping.validation.modelName, errors);
190
+ const newError: BadRequestError = new BadRequestError(...errors);
191
+ throw newError;
192
+ }
193
+ }
194
+ } else {
195
+ Logger.debug('No validation since no route specified or no parsed body');
196
+ }
197
+ return true;
198
+ }
199
+
200
+ public static async validateInboundQueryParams(_fCtx: FilterChainContext): Promise<boolean> {
201
+ // TODO: Implement ME!
202
+ return true;
203
+ }
204
+
205
+ public static async validateInboundPathParams(_fCtx: FilterChainContext): Promise<boolean> {
206
+ // TODO: Implement ME!
207
+ return true;
208
+ }
209
+
210
+ public static async validateOutboundResponse(fCtx: FilterChainContext): Promise<boolean> {
211
+ // Use !== true below because commonly it just wont be spec'd
212
+ if (fCtx?.rawResult) {
213
+ if (fCtx.routeAndParse.mapping.outboundValidation) {
214
+ Logger.debug('Applying outbound check to %j', fCtx.rawResult);
215
+ const errors: string[] = fCtx.modelValidator.validate(
216
+ fCtx.routeAndParse.mapping.outboundValidation.modelName,
217
+ fCtx.rawResult,
218
+ fCtx.routeAndParse.mapping.outboundValidation.emptyAllowed,
219
+ fCtx.routeAndParse.mapping.outboundValidation.extraPropertiesAllowed,
220
+ );
221
+ if (errors.length > 0) {
222
+ Logger.error(
223
+ 'Found outbound errors while validating %s object %j',
224
+ fCtx.routeAndParse.mapping.outboundValidation.modelName,
225
+ errors,
226
+ );
227
+ errors.unshift('Server sent object invalid according to spec');
228
+ throw new RestfulApiHttpError().withErrors(errors).withHttpStatusCode(500).withDetails(fCtx.rawResult);
229
+ }
230
+ } else {
231
+ Logger.debug('Applied no outbound validation because none set');
232
+ }
233
+ } else {
234
+ Logger.debug('No validation since no outbound body or disabled');
235
+ }
236
+ return true;
237
+ }
238
+
239
+ public static async autoRespondToOptionsRequestWithCors(
240
+ fCtx: FilterChainContext,
241
+ corsMethod: EpsilonCorsApproach = EpsilonCorsApproach.Reflective,
242
+ ): Promise<boolean> {
243
+ if (StringRatchet.trimToEmpty(fCtx?.event?.httpMethod).toLowerCase() === 'options') {
244
+ fCtx.result = {
245
+ statusCode: 200,
246
+ body: '{"cors":true, "m":3}',
247
+ };
248
+ await BuiltInFilters.addCorsHeadersDynamically(fCtx, corsMethod);
249
+ return false;
250
+ } else {
251
+ return true;
252
+ }
253
+ }
254
+
255
+ public static async autoRespond(fCtx: FilterChainContext, inBody: any): Promise<boolean> {
256
+ const body: any = inBody || {
257
+ message: 'Not Implemented',
258
+ };
259
+ fCtx.result = {
260
+ statusCode: 200,
261
+ body: JSON.stringify(body),
262
+ };
263
+ return false;
264
+ }
265
+
266
+ public static async secureOutboundServerErrorForProduction(
267
+ fCtx: FilterChainContext,
268
+ errorMessage: string,
269
+ errCode: number,
270
+ ): Promise<boolean> {
271
+ if (fCtx?.result?.statusCode) {
272
+ if (errCode === null || fCtx.result.statusCode === errCode) {
273
+ Logger.warn('Securing outbound error info (was : %j)', fCtx.result.body);
274
+ fCtx.rawResult = new RestfulApiHttpError(errorMessage).withHttpStatusCode(fCtx.result.statusCode);
275
+ const oldResult: APIGatewayProxyResult = fCtx.result;
276
+ fCtx.result = ResponseUtil.errorResponse(fCtx.rawResult);
277
+ // Need this to preserve any CORS headers, etc
278
+ fCtx.result.headers = Object.assign({}, oldResult.headers || {}, fCtx.result.headers || {});
279
+ }
280
+ }
281
+ return true;
282
+ }
283
+
284
+ public static async addCorsHeadersDynamically(fCtx: FilterChainContext, corsMethod: EpsilonCorsApproach): Promise<void> {
285
+ if (corsMethod) {
286
+ switch (corsMethod) {
287
+ case EpsilonCorsApproach.All:
288
+ await BuiltInFilters.addAllowEverythingCORSHeaders(fCtx);
289
+ break;
290
+ case EpsilonCorsApproach.Reflective:
291
+ await BuiltInFilters.addAllowReflectionCORSHeaders(fCtx);
292
+ break;
293
+ default: // Also NONE
294
+ // Do nothing
295
+ }
296
+ } else {
297
+ Logger.warn('Called add CORS headers dynamically but no type supplied, using NONE');
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,85 @@
1
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
2
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
3
+ import { RestfulApiHttpError } from '@bitblit/ratchet-common/network/restful-api-http-error';
4
+ import { NumberRatchet } from '@bitblit/ratchet-common/lang/number-ratchet';
5
+ import { APIGatewayEvent, Context } from 'aws-lambda';
6
+ import { ExtendedAPIGatewayEvent } from '../../config/http/extended-api-gateway-event.js';
7
+ import { BadRequestError } from '../../http/error/bad-request-error.js';
8
+ import { EpsilonRouter } from '../../http/route/epsilon-router.js';
9
+ import { UnauthorizedError } from '../../http/error/unauthorized-error.js';
10
+ import { NotFoundError } from '../../http/error/not-found-error.js';
11
+ import { ForbiddenError } from '../../http/error/forbidden-error.js';
12
+ import { NotImplemented } from '../../http/error/not-implemented.js';
13
+ import { MisconfiguredError } from '../../http/error/misconfigured-error.js';
14
+
15
+ export class BuiltInHandlers {
16
+ public static async expectedHandledByFilter(evt: ExtendedAPIGatewayEvent, _flag?: string): Promise<any> {
17
+ throw new MisconfiguredError().withFormattedErrorMessage(
18
+ 'Should not happen - it was expected that route %s would be handled by a filter',
19
+ evt.path,
20
+ );
21
+ }
22
+
23
+ public static async handleNotImplemented(evt: ExtendedAPIGatewayEvent, _flag?: string): Promise<any> {
24
+ Logger.info('A request was made to %s with body %j - not yet implemented', evt.path, evt.body);
25
+
26
+ const rval: any = {
27
+ time: new Date().toLocaleString(),
28
+ path: evt.path,
29
+ message: 'NOT IMPLEMENTED YET',
30
+ };
31
+
32
+ return rval;
33
+ }
34
+
35
+ public static async sample(evt: ExtendedAPIGatewayEvent, flag?: string, context?: Context): Promise<any> {
36
+ const rval: any = {
37
+ time: new Date().toLocaleString(),
38
+ evt: evt,
39
+ pad: StringRatchet.createRandomHexString(2000),
40
+ flag: flag,
41
+ };
42
+
43
+ if (context) {
44
+ rval['context'] = context;
45
+ }
46
+
47
+ const errNumber: number = NumberRatchet.safeNumber(evt.queryStringParameters['error']);
48
+ if (errNumber) {
49
+ switch (errNumber) {
50
+ case -1:
51
+ throw new Error('Test random failure');
52
+ case 400:
53
+ throw new BadRequestError('Bad request error');
54
+ case 401:
55
+ throw new UnauthorizedError('Unauthorized error');
56
+ case 403:
57
+ throw new ForbiddenError('Forbidden error');
58
+ case 404:
59
+ throw new NotFoundError('Not Found error');
60
+ case 501:
61
+ throw new NotImplemented('Not Implemented');
62
+ default:
63
+ throw new RestfulApiHttpError<any>()
64
+ .withFormattedErrorMessage('Default error - %s', errNumber)
65
+ .withHttpStatusCode(500)
66
+ .withDetails({ src: errNumber })
67
+ .withEndUserErrors(['msg1', 'msg2']);
68
+ }
69
+ }
70
+
71
+ let test: string = StringRatchet.trimToNull(evt.queryStringParameters['test']);
72
+ if (test) {
73
+ test = test.toLowerCase();
74
+ if (test === 'null') {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ return rval;
80
+ }
81
+
82
+ public static async defaultErrorProcessor(event: APIGatewayEvent, err: Error, cfg: EpsilonRouter): Promise<void> {
83
+ Logger.warn('Unhandled error (in promise catch) : %s \nStack was: %s\nEvt was: %j\nConfig was: %j', err.message, err.stack, event, cfg);
84
+ }
85
+ }