All files / src/core/server cors.ts

78.26% Statements 18/23
67.85% Branches 19/28
100% Functions 3/3
80.95% Lines 17/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88                                          4x 4x                       9x 4x                 5x 5x                     8x 3x       3x             8x 8x   8x         8x     8x     8x 1x   8x    
/**
 * CORS header helpers for the request handler
 */
 
import type { IncomingMessage } from 'http';
 
export interface CorsOptions {
  /** Allowed origin(s). Use '*' for any, or a specific origin. */
  origin?: string | string[];
  /** Allowed methods. Default: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS */
  methods?: string[];
  /** Allowed request headers. */
  allowedHeaders?: string[];
  /** Headers exposed to the browser. */
  exposeHeaders?: string[];
  /** Allow credentials (cookies, authorization). When true, origin cannot be '*'. */
  credentials?: boolean;
  /** Max age in seconds for preflight cache. */
  maxAge?: number;
}
 
const DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
const DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'];
 
export interface NormalizedCorsOptions {
  origin: string | string[];
  methods: string[];
  allowedHeaders: string[];
  exposeHeaders: string[];
  credentials: boolean;
  maxAge: number | undefined;
}
 
export function normalizeCorsOptions(cors: boolean | CorsOptions): NormalizedCorsOptions {
  if (cors === true) {
    return {
      origin: '*',
      methods: DEFAULT_METHODS,
      allowedHeaders: DEFAULT_ALLOWED_HEADERS,
      exposeHeaders: [],
      credentials: false,
      maxAge: undefined,
    };
  }
  const opts = cors as CorsOptions;
  return {
    origin: opts.origin ?? '*',
    methods: opts.methods ?? DEFAULT_METHODS,
    allowedHeaders: opts.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS,
    exposeHeaders: opts.exposeHeaders ?? [],
    credentials: opts.credentials ?? false,
    maxAge: opts.maxAge,
  };
}
 
function resolveOrigin(requestOrigin: string | undefined, option: string | string[]): string {
  if (option === '*') return '*';
  Iif (Array.isArray(option)) {
    if (requestOrigin && option.includes(requestOrigin)) return requestOrigin;
    return option[0] ?? '*';
  }
  return option;
}
 
export function getCorsHeaders(
  req: IncomingMessage,
  options: NormalizedCorsOptions
): Record<string, string> {
  const requestOrigin = req.headers.origin as string | undefined;
  const origin = resolveOrigin(requestOrigin, options.origin);
 
  const headers: Record<string, string> = {
    'Access-Control-Allow-Origin': origin,
    'Access-Control-Allow-Methods': options.methods.join(', '),
    'Access-Control-Allow-Headers': options.allowedHeaders.join(', '),
  };
  Iif (options.exposeHeaders.length > 0) {
    headers['Access-Control-Expose-Headers'] = options.exposeHeaders.join(', ');
  }
  Iif (options.credentials) {
    headers['Access-Control-Allow-Credentials'] = 'true';
  }
  if (options.maxAge != null) {
    headers['Access-Control-Max-Age'] = String(options.maxAge);
  }
  return headers;
}