import type { CookieRewriteOptions } from './types' const SECURE_COOKIE_PREFIX_PATTERN = /^__(Host|Secure)-/ const LOCAL_SCOPED_COOKIE_PREFIX = 'dev_proxy' const SAME_SITE_NONE_PATTERN = /^samesite=none$/i const COOKIE_PATH_PATTERN = /^path=/i const COOKIE_DOMAIN_PATTERN = /^domain=/i const COOKIE_SECURE_PATTERN = /^secure$/i const COOKIE_PARTITIONED_PATTERN = /^partitioned$/i const stripSecureCookiePrefix = (cookieName: string) => cookieName.replace(SECURE_COOKIE_PREFIX_PATTERN, '') const matchesCookieName = (cookieName: string, matcher: string | RegExp) => typeof matcher === 'string' ? matcher === cookieName : matcher.test(cookieName) const hashScope = (scope: string) => { let hash = 0x811C9DC5 for (let index = 0; index < scope.length; index += 1) { hash ^= scope.charCodeAt(index) hash = Math.imul(hash, 0x01000193) } return (hash >>> 0).toString(36) } const shouldUseHostPrefix = (cookieName: string, options: CookieRewriteOptions) => { const normalizedCookieName = stripSecureCookiePrefix(cookieName) return options.hostPrefixCookies?.some(matcher => matchesCookieName(normalizedCookieName, matcher)) || false } const toUpstreamCookieName = (cookieName: string, options: CookieRewriteOptions) => { if (cookieName.startsWith('__Host-')) return cookieName if (cookieName.startsWith('__Secure-')) return `__Host-${stripSecureCookiePrefix(cookieName)}` if (!shouldUseHostPrefix(cookieName, options)) return cookieName return `__Host-${cookieName}` } export const toLocalCookieName = (cookieName: string) => stripSecureCookiePrefix(cookieName) export const resolveCookieRewriteLocalScopeKey = (options: CookieRewriteOptions, targetUrl: URL) => { if (options.localCookieScope === 'target-origin') return hashScope(targetUrl.origin) return undefined } export const toScopedLocalCookieName = (cookieName: string, localScopeKey: string) => `${LOCAL_SCOPED_COOKIE_PREFIX}_${localScopeKey}_${toLocalCookieName(cookieName)}` const fromScopedLocalCookieName = (cookieName: string, localScopeKey: string) => { const scopedPrefix = `${LOCAL_SCOPED_COOKIE_PREFIX}_${localScopeKey}_` if (!cookieName.startsWith(scopedPrefix)) return undefined return cookieName.slice(scopedPrefix.length) } const isScopedLocalCookieName = (cookieName: string) => cookieName.startsWith(`${LOCAL_SCOPED_COOKIE_PREFIX}_`) const parseCookieHeader = (cookieHeader: string | undefined) => { if (!cookieHeader) return [] return cookieHeader .split(/;\s*/) .filter(Boolean) .map((cookie) => { const separatorIndex = cookie.indexOf('=') if (separatorIndex === -1) return { name: cookie, value: undefined } return { name: cookie.slice(0, separatorIndex).trim(), value: cookie.slice(separatorIndex + 1), } }) } export const getCookieHeaderValue = (cookieHeader: string | undefined, cookieName: string) => { const cookie = parseCookieHeader(cookieHeader).find(cookie => cookie.name === cookieName) return cookie?.value } export const rewriteCookieHeaderForUpstream = ( cookieHeader: string | undefined, options: CookieRewriteOptions & { useHostPrefix?: boolean, localScopeKey?: string }, ) => { if (!cookieHeader) return cookieHeader const { useHostPrefix = true } = options return parseCookieHeader(cookieHeader) .map((cookie) => { if (cookie.value === undefined) return cookie.name const scopedCookieName = options.localScopeKey ? fromScopedLocalCookieName(cookie.name, options.localScopeKey) : undefined if (scopedCookieName) { const upstreamCookieName = useHostPrefix ? toUpstreamCookieName(scopedCookieName, options) : scopedCookieName return `${upstreamCookieName}=${cookie.value}` } if (options.localScopeKey && (isScopedLocalCookieName(cookie.name) || shouldUseHostPrefix(cookie.name, options))) return undefined const upstreamCookieName = useHostPrefix ? toUpstreamCookieName(cookie.name, options) : cookie.name return `${upstreamCookieName}=${cookie.value}` }) .filter((cookie): cookie is string => Boolean(cookie)) .join('; ') } const rewriteSetCookieValueForLocal = ( setCookieValue: string, options?: CookieRewriteOptions & { localScopeKey?: string }, ) => { const [rawCookiePair, ...rawAttributes] = setCookieValue.split(';') const separatorIndex = rawCookiePair!.indexOf('=') if (separatorIndex === -1) return setCookieValue const cookieName = rawCookiePair!.slice(0, separatorIndex).trim() const cookieValue = rawCookiePair!.slice(separatorIndex + 1) const localCookieName = toLocalCookieName(cookieName) const shouldScopeCookie = Boolean( options?.localScopeKey && shouldUseHostPrefix(cookieName, options), ) const rewrittenCookieName = shouldScopeCookie ? toScopedLocalCookieName(cookieName, options!.localScopeKey!) : localCookieName const rewrittenAttributes = rawAttributes .map(attribute => attribute.trim()) .filter(attribute => !COOKIE_DOMAIN_PATTERN.test(attribute) && !COOKIE_SECURE_PATTERN.test(attribute) && !COOKIE_PARTITIONED_PATTERN.test(attribute), ) .map((attribute) => { if (SAME_SITE_NONE_PATTERN.test(attribute)) return 'SameSite=Lax' if (COOKIE_PATH_PATTERN.test(attribute)) return 'Path=/' return attribute }) return [`${rewrittenCookieName}=${cookieValue}`, ...rewrittenAttributes].join('; ') } export const rewriteSetCookieHeadersForLocal = ( setCookieHeaders: readonly string[], options?: CookieRewriteOptions & { localScopeKey?: string }, ) => setCookieHeaders.map(cookie => rewriteSetCookieValueForLocal(cookie, options))