feat(middleware-cors): add cors support
ADD
packages/middleware-cors/README.md
+Middleware Cors
+=======
+
+A [joi](https://github.com/hapijs/joi) validation middleware for [Marble.js](https://github.com/marblejs/marble).
+
+## Usage
+
+Example of using this middleware on a GET route to validate params.
+
+```javascript
+import { validator$, Joi } from '@marblejs/middleware-joi';
+
+const foo$ = EffectFactory
+ .matchPath('/foo/:id')
+ .matchType('GET')
+ .use(req$ => req$.pipe(
+ use(validator$({
+ params: Joi.object({
+ id: Joi.number().min(1).max(10),
+ })
+ }));
+ // ...
+ ));
+```
+
+Example to validate all incoming requests.
+
+```javascript
+import { validator$, Joi } from '@marblejs/middleware-joi';
+
+const middlewares = [
+ logger$,
+ validator$({
+ headers: Joi.object({
+ sign: Joi.string(),
+ accept: Joi.string().default('application/json'),
+ }),
+ params: Joi.object({
+ apiKey: Joi.string().token().required(),
+ })
+ })
+];
+
+const effects = [
+ endpoint1$,
+ endpoint2$,
+ ...
+];
+
+const app = httpListener({ middlewares, effects });
+```
+
+License: MIT

ADD
packages/middleware-cors/src/configureHeaders.ts
+import { HttpRequest, HttpResponse } from '@marblejs/core';
+
+import { capitalize, isNotEmptyArray, isString } from './util';
+import { CORSOptions } from './middleware';
+
+interface ConfiguredHeader {
+ key: string;
+ value: string;
+}
+
+enum AccessControlAllow {
+ Origin = 'Access-Control-Allow-Origin',
+ Methods = 'Access-Control-Allow-Methods',
+ Headers = 'Access-Control-Allow-Headers',
+ Credentials = 'Access-Control-Allow-Credentials',
+}
+
+const configureAllowedOrigin = (
+ req: HttpRequest,
+ allowedOrigin: string | string[] | RegExp,
+): ConfiguredHeader[] => {
+ const headers: ConfiguredHeader[] = [];
+ const origin = req.headers.origin as string;
+
+ if (isString(allowedOrigin) && allowedOrigin === '*') {
+ headers.push({ key: AccessControlAllow.Origin, value: '*' });
+ } else if (
+ isString(allowedOrigin) &&
+ allowedOrigin !== '*' &&
+ origin.match(allowedOrigin as string)
+ ) {
+ headers.push({ key: AccessControlAllow.Origin, value: origin });
+ } else if (
+ isNotEmptyArray(allowedOrigin) &&
+ (allowedOrigin as Array<string>).includes(origin)
+ ) {
+ headers.push({ key: AccessControlAllow.Origin, value: origin });
+ } else if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
+ headers.push({ key: AccessControlAllow.Origin, value: origin });
+ }
+
+ return headers;
+};
+
+const configureAllowedMethods = (
+ req: HttpRequest,
+ methods: string[],
+): ConfiguredHeader[] => {
+ const headers: ConfiguredHeader[] = [];
+
+ if (isNotEmptyArray(methods) && methods.includes(req.method)) {
+ headers.push({
+ key: AccessControlAllow.Methods,
+ value: methods.join(', '),
+ });
+ }
+
+ return headers;
+};
+
+const configureAllowedHeaders = (
+ allowedHeaders: string | string[],
+): ConfiguredHeader[] => {
+ const headers: ConfiguredHeader[] = [];
+
+ if (isString(allowedHeaders) && allowedHeaders === '*') {
+ headers.push({
+ key: AccessControlAllow.Headers,
+ value: '*',
+ });
+ } else if (isNotEmptyArray(allowedHeaders)) {
+ headers.push({
+ key: AccessControlAllow.Headers,
+ value: (allowedHeaders as Array<string>)
+ .map(header => capitalize(header))
+ .join(', '),
+ });
+ }
+
+ return headers;
+};
+
+const configureCredentials = (withCredentials: boolean): ConfiguredHeader[] => {
+ if (withCredentials) {
+ return [{ key: AccessControlAllow.Credentials, value: 'true' }];
+ }
+
+ return [];
+};
+
+const applyHeaders = (headers: ConfiguredHeader[], res: HttpResponse): void => {
+ headers.forEach(({ key, value }) => {
+ res.setHeader(key, value);
+ });
+};
+
+export function configureHeaders(
+ req: HttpRequest,
+ res: HttpResponse,
+ options: CORSOptions,
+): void {
+ // @todo add exposed headers
+ const headers = [
+ ...configureAllowedOrigin(req, options.origin!),
+ ...configureCredentials(options.withCredentials!),
+ ];
+
+ if (req.method === 'OPTIONS') {
+ // @todo add max age header, exposed headers
+ const preflightHeaders = [
+ ...configureAllowedMethods(req, options.methods!),
+ ...configureAllowedHeaders(options.allowHeaders!),
+ { key: 'Content-Length', value: '0' },
+ ];
+
+ headers.push(...preflightHeaders);
+
+ res.statusCode = options.optionsSuccessStatus!;
+ }
+
+ applyHeaders(headers, res);
+}

ADD
packages/middleware-cors/src/index.ts
+
+export { cors$ } from './middleware';

ADD
packages/middleware-cors/src/middleware.ts
+import { HttpError, HttpStatus, Middleware, HttpMethod } from '@marblejs/core';
+import { throwError } from 'rxjs';
+import { catchError, tap } from 'rxjs/operators';
+
+import { configureHeaders } from './configureHeaders';
+
+export interface CORSOptions {
+ origin?: string | string[] | RegExp;
+ methods?: HttpMethod[];
+ optionsSuccessStatus?: HttpStatus;
+ allowHeaders?: string | string[];
+ withCredentials?: boolean;
+}
+
+const DEFAULT_OPTIONS: CORSOptions = {
+ origin: '*',
+ methods: ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
+ withCredentials: false,
+ optionsSuccessStatus: HttpStatus.NO_CONTENT,
+ allowHeaders: '*',
+};
+
+export const cors$ = (options: CORSOptions = {}): Middleware => (req$, res) => {
+ options = { ...DEFAULT_OPTIONS, ...options };
+
+ return req$.pipe(
+ tap(req => {
+ configureHeaders(req, res, options);
+
+ if (req.method === 'OPTIONS') {
+ res.end();
+ }
+ }),
+ catchError(() =>
+ throwError(new HttpError('Not allowed by CORS', HttpStatus.BAD_REQUEST)),
+ ),
+ );
+};

ADD
packages/middleware-cors/src/util.ts
+export const capitalize = (str: string): string =>
+ str
+ .split('-')
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
+ .join('-');
+
+export const isString = (str: any): boolean =>
+ typeof str === 'string' || str instanceof String;
+
+export const isNotEmptyArray = (arr: any) => Array.isArray(arr) && arr.length > 0;

ADD
packages/middleware-cors/tsconfig.json
+{
+ "extends": "../../tsconfig.json",
+
+ "compilerOptions": {
+ "outDir": "./dist/",
+ "declaration": true,
+ "typeRoots": ["../../node_modules/@types"]
+ },
+
+ "include": ["src/**/*.ts"]
+}

feat(middleware-cors): add cors support