| 81 | } |
| 82 | |
| 83 | export function getCorsOptions(): any { |
| 84 | return (req: any, callback: (err: Error | null, options?: any) => void) => { |
| 85 | const allowedOrigins = getAllowedCorsOrigins() |
| 86 | const requestedCredentials = getAllowCredentials() |
| 87 | const credentials = allowedOrigins === '*' ? false : requestedCredentials |
| 88 | |
| 89 | const corsOptions = { |
| 90 | credentials, |
| 91 | origin: async (origin: string | undefined, originCallback: (err: Error | null, allow?: boolean) => void) => { |
| 92 | const isPublicChatflowReq = isPublicChatflowRequest(req.url) |
| 93 | const isTTSReq = isTTSGenerateRequest(req.url) |
| 94 | const allowedList = parseAllowedOrigins(allowedOrigins) |
| 95 | const originLc = origin?.toLowerCase() |
| 96 | |
| 97 | // Always allow no-Origin requests (same-origin, server-to-server) |
| 98 | if (!originLc) return originCallback(null, true) |
| 99 | |
| 100 | // Block null origins (sandboxed iframes, data: URIs, file:// pages) |
| 101 | if (originLc === 'null') return originCallback(null, false) |
| 102 | |
| 103 | // Session-issuing endpoints: ignore global wildcard, use APP_URL origin or explicit CORS_ORIGINS list |
| 104 | if (isSessionEndpoint(req.url)) { |
| 105 | const authList = getAllowedAuthCorsOrigins() |
| 106 | return originCallback(null, authList.includes(originLc) || allowedList.includes(originLc)) |
| 107 | } |
| 108 | |
| 109 | // Global allow: '*' or exact match |
| 110 | const globallyAllowed = allowedOrigins === '*' || allowedList.includes(originLc) |
| 111 | |
| 112 | if (isPublicChatflowReq || isTTSReq) { |
| 113 | // Per-chatflow allowlist OR globally allowed |
| 114 | // TTS generate passes chatflowId in the request body, not the URL path |
| 115 | const chatflowId = isTTSReq ? req.body?.chatflowId : extractChatflowId(req.url) |
| 116 | let chatflowAllowed = false |
| 117 | if (chatflowId) { |
| 118 | try { |
| 119 | chatflowAllowed = await validateChatflowDomain(chatflowId, originLc, req.user?.activeWorkspaceId) |
| 120 | } catch (error) { |
| 121 | // Log error and deny on failure |
| 122 | console.error('Domain validation error:', error) |
| 123 | chatflowAllowed = false |
| 124 | } |
| 125 | } else if (isTTSReq) { |
| 126 | // OPTIONS preflight has no body — allow it through so the actual POST can be validated with chatflowId |
| 127 | chatflowAllowed = true |
| 128 | } |
| 129 | return originCallback(null, globallyAllowed || chatflowAllowed) |
| 130 | } |
| 131 | |
| 132 | // Non-prediction: rely on global policy only |
| 133 | return originCallback(null, globallyAllowed) |
| 134 | } |
| 135 | } |
| 136 | callback(null, corsOptions) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | /** |