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) => ( // oxlint-disable-next-line react/no-array-index-key {cookie.value} ))} )} Received Cookies } > {receivedCookies.length === 0 ? ( ) : (
{receivedCookies.map((cookie, i) => ( // oxlint-disable-next-line react/no-array-index-key
{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; }