import type { HttpResponse } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { useMemo } from 'react'; import type { JSX } from 'react/jsx-runtime'; import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { CountBadge } from './core/CountBadge'; import { DetailsBanner } from './core/DetailsBanner'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; interface Props { response: HttpResponse; } interface ParsedCookie { name: string; value: string; domain?: string; path?: string; expires?: string; maxAge?: string; secure?: boolean; httpOnly?: boolean; sameSite?: string; isDeleted?: boolean; } function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> { // Parse "Cookie: name=value; name2=value2" format return cookieHeader.split(';').map((pair) => { const [name = '', ...valueParts] = pair.split('='); return { name: name.trim(), value: valueParts.join('=').trim(), }; }); } function parseSetCookieHeader(setCookieHeader: string): ParsedCookie { // Parse "Set-Cookie: name=value; Domain=...; Path=..." format const parts = setCookieHeader.split(';').map((p) => p.trim()); const [nameValue = '', ...attributes] = parts; const [name = '', ...valueParts] = nameValue.split('='); const cookie: ParsedCookie = { name: name.trim(), value: valueParts.join('=').trim(), }; for (const attr of attributes) { const [key = '', val] = attr.split('=').map((s) => s.trim()); const lowerKey = key.toLowerCase(); if (lowerKey === 'domain') cookie.domain = val; else if (lowerKey === 'path') cookie.path = val; else if (lowerKey === 'expires') cookie.expires = val; else if (lowerKey === 'max-age') cookie.maxAge = val; else if (lowerKey === 'secure') cookie.secure = true; else if (lowerKey === 'httponly') cookie.httpOnly = true; else if (lowerKey === 'samesite') cookie.sameSite = val; } // Detect if cookie is being deleted if (cookie.maxAge !== undefined) { const maxAgeNum = Number.parseInt(cookie.maxAge, 10); if (!Number.isNaN(maxAgeNum) && maxAgeNum <= 0) { cookie.isDeleted = true; } } else if (cookie.expires !== undefined) { // Check if expires date is in the past try { const expiresDate = new Date(cookie.expires); if (expiresDate.getTime() < Date.now()) { cookie.isDeleted = true; } } catch { // Invalid date, ignore } } return cookie; } export function ResponseCookies({ response }: Props) { const { data: events } = useHttpResponseEvents(response); const { sentCookies, receivedCookies } = useMemo(() => { if (!events) return { sentCookies: [], receivedCookies: [] }; // Use Maps to deduplicate by cookie name (latest value wins) const sentMap = new Map(); const receivedMap = new Map(); for (const event of events) { const e = event.event; // Cookie headers sent (header_up with name=cookie) if (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') { const cookies = parseCookieHeader(e.value); for (const cookie of cookies) { sentMap.set(cookie.name, cookie); } } // Set-Cookie headers received (header_down with name=set-cookie) if (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') { const cookie = parseSetCookieHeader(e.value); receivedMap.set(cookie.name, cookie); } } return { sentCookies: Array.from(sentMap.values()), receivedCookies: Array.from(receivedMap.values()), }; }, [events]); return (
Sent Cookies } > {sentCookies.length === 0 ? ( ) : ( {sentCookies.map((cookie, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: none {cookie.value} ))} )} Received Cookies } > {receivedCookies.length === 0 ? ( ) : (
{receivedCookies.map((cookie, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: none
{cookie.name} = {cookie.value} {cookie.isDeleted && ( Deleted )}
{[ cookie.domain && ( {cookie.domain} ), cookie.path && ( {cookie.path} ), cookie.expires && ( {cookie.expires} ), cookie.maxAge && ( {cookie.maxAge} ), cookie.secure && ( true ), cookie.httpOnly && ( true ), cookie.sameSite && ( {cookie.sameSite} ), ].filter((item): item is JSX.Element => Boolean(item))}
))}
)}
); } function NoCookies() { return No Cookies; }