     1	export const CorsPlugin = {
     2	  name: 'rest-api-cors',
     3	  dependencies: ['rest-api'],
     4	
     5	  async install ({ api, addHook, vars, helpers, log, pluginOptions, runHooks }) {
     6	    // Get CORS configuration
     7	    const corsOptions = pluginOptions || {}
     8	
     9	    // Store configuration - using a plain object for runtime updates
    10	    const corsConfig = {
    11	      // Origin configuration
    12	      origin: corsOptions.origin || '*', // Can be string, regex, array, or function
    13	      credentials: corsOptions.credentials !== undefined ? corsOptions.credentials : true,
    14	
    15	      // Allowed methods
    16	      methods: corsOptions.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    17	
    18	      // Allowed headers
    19	      allowedHeaders: corsOptions.allowedHeaders || [
    20	        'Content-Type',
    21	        'Authorization',
    22	        'X-Requested-With',
    23	        'X-HTTP-Method-Override',
    24	        'Accept',
    25	        'Origin'
    26	      ],
    27	
    28	      // Exposed headers (for client to read)
    29	      exposedHeaders: corsOptions.exposedHeaders || [
    30	        'X-Total-Count',
    31	        'X-Page-Count',
    32	        'Link',
    33	        'Location'
    34	      ],
    35	
    36	      // Preflight cache
    37	      maxAge: corsOptions.maxAge || 86400, // 24 hours
    38	
    39	      // Options success status
    40	      optionsSuccessStatus: corsOptions.optionsSuccessStatus || 204
    41	    }
    42	
    43	    // Store reference in vars for access in hooks
    44	    vars.cors = corsConfig
    45	
    46	    // Helper to check if origin is allowed
    47	    function isOriginAllowed (origin, allowedOrigin) {
    48	      if (!origin) return !corsConfig.credentials // No origin = same-origin request
    49	
    50	      // String match
    51	      if (typeof allowedOrigin === 'string') {
    52	        return allowedOrigin === '*' || allowedOrigin === origin
    53	      }
    54	
    55	      // Regex match
    56	      if (allowedOrigin instanceof RegExp) {
    57	        return allowedOrigin.test(origin)
    58	      }
    59	
    60	      // Array of allowed origins
    61	      if (Array.isArray(allowedOrigin)) {
    62	        return allowedOrigin.some(allowed => isOriginAllowed(origin, allowed))
    63	      }
    64	
    65	      // Function check
    66	      if (typeof allowedOrigin === 'function') {
    67	        return allowedOrigin(origin)
    68	      }
    69	
    70	      return false
    71	    }
    72	
    73	    // Check if transport plugin is available
    74	    if (!vars.transport) {
    75	      log.error('CORS plugin requires a transport plugin (express, koa, etc.) to be installed')
    76	      return
    77	    }
    78	
    79	    // Register OPTIONS handler for all routes
    80	    const optionsHandler = async ({ context, headers }) => {
    81	      const origin = headers?.origin
    82	
    83	      log.debug('CORS OPTIONS request', { origin })
    84	
    85	      // Check if origin is allowed
    86	      if (isOriginAllowed(origin, corsConfig.origin)) {
    87	        const responseHeaders = {
    88	          'Access-Control-Allow-Methods': corsConfig.methods.join(', '),
    89	          'Access-Control-Allow-Headers': corsConfig.allowedHeaders.join(', '),
    90	          'Access-Control-Max-Age': String(corsConfig.maxAge)
    91	        }
    92	
    93	        // Set origin header
    94	        if (corsConfig.origin === '*' && !corsConfig.credentials) {
    95	          responseHeaders['Access-Control-Allow-Origin'] = '*'
    96	        } else if (origin) {
    97	          responseHeaders['Access-Control-Allow-Origin'] = origin
    98	          responseHeaders['Vary'] = 'Origin'
    99	        }
   100	
   101	        // Set credentials if enabled
   102	        if (corsConfig.credentials) {
   103	          responseHeaders['Access-Control-Allow-Credentials'] = 'true'
   104	        }
   105	
   106	        return {
   107	          statusCode: corsConfig.optionsSuccessStatus,
   108	          headers: responseHeaders,
   109	          body: null
   110	        }
   111	      } else {
   112	        // Origin not allowed
   113	        log.warn('CORS origin not allowed', { origin })
   114	        return {
   115	          statusCode: 403,
   116	          body: { error: 'CORS origin not allowed' }
   117	        }
   118	      }
   119	    }
   120	
   121	    // Register OPTIONS route for all paths
   122	    await api.addRoute({
   123	      method: 'OPTIONS',
   124	      path: vars.transport.matchAll,
   125	      handler: optionsHandler
   126	    })
   127	
   128	    // Hook to handle CORS headers for all responses
   129	    addHook('transport:response', 'cors-headers', { order: -1000 }, async ({ context }) => {
   130	      // Transport data is now nested in context
   131	      const { request, response } = context.transport || {}
   132	
   133	      if (!request) {
   134	        log.error('CORS: request is undefined in transport:response hook')
   135	        return
   136	      }
   137	
   138	      const origin = request.headers?.origin
   139	      const method = request.method?.toUpperCase()
   140	
   141	      log.debug('CORS processing response', {
   142	        origin,
   143	        method,
   144	        path: request.path
   145	      })
   146	
   147	      // Skip if this was an OPTIONS request (already handled by route)
   148	      if (method === 'OPTIONS') {
   149	        return
   150	      }
   151	
   152	      // Check if origin is allowed
   153	      if (isOriginAllowed(origin, corsConfig.origin)) {
   154	        // Set origin header
   155	        if (corsConfig.origin === '*' && !corsConfig.credentials) {
   156	          response.headers['Access-Control-Allow-Origin'] = '*'
   157	        } else if (origin) {
   158	          response.headers['Access-Control-Allow-Origin'] = origin
   159	          response.headers['Vary'] = 'Origin'
   160	        }
   161	
   162	        // Set credentials if enabled
   163	        if (corsConfig.credentials) {
   164	          response.headers['Access-Control-Allow-Credentials'] = 'true'
   165	        }
   166	
   167	        // Set exposed headers
   168	        if (corsConfig.exposedHeaders.length > 0) {
   169	          response.headers['Access-Control-Expose-Headers'] = corsConfig.exposedHeaders.join(', ')
   170	        }
   171	      } else if (origin) {
   172	        // Origin not allowed - don't set any CORS headers
   173	        log.warn('CORS origin not allowed for response', {
   174	          origin,
   175	          allowedOrigins: corsConfig.origin
   176	        })
   177	      }
   178	    })
   179	
   180	    log.info('CORS plugin installed', {
   181	      origin: corsConfig.origin,
   182	      credentials: corsConfig.credentials,
   183	      methods: corsConfig.methods
   184	    })
   185	  }
   186	}
   187	
   188	/*
   189	  Usage examples:
   190	
   191	  // Basic usage - allow all origins
   192	  await api.use(CorsPlugin);
   193	
   194	  // Specific origin
   195	  await api.use(CorsPlugin, {
   196	    'rest-api-cors': {
   197	      origin: 'https://app.example.com'
   198	    }
   199	  });
   200	
   201	  // Multiple origins
   202	  await api.use(CorsPlugin, {
   203	    'rest-api-cors': {
   204	      origin: ['https://app.example.com', 'https://admin.example.com']
   205	    }
   206	  });
   207	
   208	  // Dynamic origin with regex
   209	  await api.use(CorsPlugin, {
   210	    'rest-api-cors': {
   211	      origin: /^https:\/\/.*\.example\.com$/
   212	    }
   213	  });
   214	
   215	  // Function-based origin check
   216	  await api.use(CorsPlugin, {
   217	    'rest-api-cors': {
   218	      origin: (origin) => {
   219	        // Custom logic to determine if origin is allowed
   220	        return myAllowedOrigins.includes(origin);
   221	      }
   222	    }
   223	  });
   224	
   225	  // Full configuration
   226	  await api.use(CorsPlugin, {
   227	    'rest-api-cors': {
   228	      origin: 'https://app.example.com',
   229	      credentials: true,
   230	      methods: ['GET', 'POST', 'PATCH', 'DELETE'],
   231	      allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
   232	      exposedHeaders: ['X-Total-Count', 'X-RateLimit-Remaining'],
   233	      maxAge: 3600 // 1 hour
   234	    }
   235	  });
   236	
   237	*/
