teqo-backup/src/utilities/campaignInviteOrigin.ts
Francisco Ignácio Fontoura Solla bcac665cb5 Ship /campanha nuclei MVP with RBAC, invites, and vote estimates.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.
2026-07-18 15:01:56 -03:00

156 lines
4.9 KiB
TypeScript

import 'server-only'
import { isIP } from 'node:net'
export type CampaignInviteOriginInput = {
configuredURL?: string
environment?: string
requestOrigin?: string | null
forwardedHost?: string | null
forwardedProto?: string | null
allowLocalTLS?: boolean
}
const LOCAL_AUTHORITY_PATTERN =
/^(localhost|127\.0\.0\.1|\[::1\])(?::([0-9]{1,5}))?$/i
const DNS_HOSTNAME_PATTERN =
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
const parsePort = (rawPort: string | undefined): boolean => {
if (rawPort === undefined) return true
const port = Number(rawPort)
return Number.isInteger(port) && port >= 1 && port <= 65535
}
const parseHTTPOrigin = (rawURL: string, source: string): URL => {
if (rawURL !== rawURL.trim()) {
throw new Error(`${source} precisa conter somente uma origem exata.`)
}
let url: URL
try {
url = new URL(rawURL)
} catch {
throw new Error(`${source} precisa conter uma URL válida e absoluta.`)
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`${source} precisa usar HTTP ou HTTPS.`)
}
if (url.username || url.password) {
throw new Error(`${source} não pode conter credenciais.`)
}
if (url.pathname !== '/' || url.search || url.hash) {
throw new Error(`${source} precisa conter somente a origem, sem caminho, consulta ou fragmento.`)
}
if (url.port && !parsePort(url.port)) {
throw new Error(`${source} precisa usar uma porta válida.`)
}
return url
}
const parseRawLocalAuthority = (rawAuthority: string): RegExpMatchArray | null => {
if (rawAuthority !== rawAuthority.trim()) return null
const match = rawAuthority.match(LOCAL_AUTHORITY_PATTERN)
return match && parsePort(match[2]) ? match : null
}
const parseLocalRequestOrigin = (
value: string | null | undefined,
allowLocalTLS: boolean,
): string | undefined => {
if (!value || value !== value.trim() || /[\u0000-\u001f\u007f,]/.test(value)) return undefined
const rawMatch = value.match(
/^(https?):\/\/(localhost|127\.0\.0\.1|\[::1\])(?::([0-9]{1,5}))?\/?$/i,
)
if (!rawMatch || !parsePort(rawMatch[3])) return undefined
const url = parseHTTPOrigin(value, 'A origem do convite')
if (url.protocol === 'https:' && !allowLocalTLS) url.protocol = 'http:'
return url.origin
}
const parseLocalForwardedOrigin = ({
forwardedHost,
forwardedProto,
allowLocalTLS,
}: Pick<
CampaignInviteOriginInput,
'forwardedHost' | 'forwardedProto' | 'allowLocalTLS'
>): string | undefined => {
if (!forwardedHost || !parseRawLocalAuthority(forwardedHost)) return undefined
if (
forwardedProto &&
(forwardedProto !== forwardedProto.trim() ||
/[\u0000-\u001f\u007f,]/.test(forwardedProto) ||
!/^(?:http|https)$/i.test(forwardedProto))
) {
return undefined
}
const protocol = forwardedProto?.toLowerCase() || 'http'
return parseLocalRequestOrigin(
`${protocol}://${forwardedHost}`,
Boolean(allowLocalTLS),
)
}
const requireProductionDNSOrigin = (configuredURL: string): string => {
const url = parseHTTPOrigin(configuredURL, 'NEXT_PUBLIC_SITE_URL')
if (url.protocol !== 'https:') {
throw new Error('NEXT_PUBLIC_SITE_URL precisa usar HTTPS em produção.')
}
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '')
const forbiddenHostname =
hostname === 'localhost' ||
hostname.startsWith('localhost.') ||
/^(?:[0-9]{1,3}\.){4}/.test(hostname) ||
hostname.endsWith('.localhost') ||
hostname.endsWith('.local') ||
hostname.endsWith('.localdomain') ||
hostname.endsWith('.internal')
if (isIP(hostname) !== 0 || forbiddenHostname || !DNS_HOSTNAME_PATTERN.test(hostname)) {
throw new Error(
'NEXT_PUBLIC_SITE_URL precisa apontar para um nome DNS público em produção.',
)
}
return url.origin
}
export const getCampaignInviteBaseURL = (input: CampaignInviteOriginInput = {}): string => {
const environment = input.environment ?? process.env.NODE_ENV
const configuredURL = Object.hasOwn(input, 'configuredURL')
? input.configuredURL
: process.env.NEXT_PUBLIC_SITE_URL
if (environment === 'production') {
if (!configuredURL) {
throw new Error('NEXT_PUBLIC_SITE_URL precisa ser configurada em produção.')
}
return requireProductionDNSOrigin(configuredURL)
}
if (environment === 'development' || environment === 'test') {
const requestURL =
parseLocalRequestOrigin(input.requestOrigin, Boolean(input.allowLocalTLS)) ??
parseLocalForwardedOrigin(input)
if (requestURL) return requestURL
}
if (configuredURL) {
const url = parseHTTPOrigin(configuredURL, 'NEXT_PUBLIC_SITE_URL')
if (
(url.hostname === 'localhost' ||
url.hostname === '127.0.0.1' ||
url.hostname === '[::1]') &&
url.protocol === 'https:' &&
!input.allowLocalTLS
) {
url.protocol = 'http:'
}
return url.origin
}
throw new Error('Não foi possível determinar uma origem segura para o convite.')
}