mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-20 16:43:53 +01:00
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
7.6 KiB
TypeScript
226 lines
7.6 KiB
TypeScript
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<string, { name: string; value: string }>();
|
|
const receivedMap = new Map<string, ParsedCookie>();
|
|
|
|
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 (
|
|
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
|
<DetailsBanner
|
|
defaultOpen
|
|
storageKey={`${response.requestId}.sent_cookies`}
|
|
summary={
|
|
<h2 className="flex items-center">
|
|
Sent Cookies <CountBadge showZero count={sentCookies.length} />
|
|
</h2>
|
|
}
|
|
>
|
|
{sentCookies.length === 0 ? (
|
|
<NoCookies />
|
|
) : (
|
|
<KeyValueRows>
|
|
{sentCookies.map((cookie, i) => (
|
|
// oxlint-disable-next-line react/no-array-index-key
|
|
<KeyValueRow labelColor="primary" key={i} label={cookie.name}>
|
|
{cookie.value}
|
|
</KeyValueRow>
|
|
))}
|
|
</KeyValueRows>
|
|
)}
|
|
</DetailsBanner>
|
|
|
|
<DetailsBanner
|
|
defaultOpen
|
|
storageKey={`${response.requestId}.received_cookies`}
|
|
summary={
|
|
<h2 className="flex items-center">
|
|
Received Cookies <CountBadge showZero count={receivedCookies.length} />
|
|
</h2>
|
|
}
|
|
>
|
|
{receivedCookies.length === 0 ? (
|
|
<NoCookies />
|
|
) : (
|
|
<div className="flex flex-col gap-4">
|
|
{receivedCookies.map((cookie, i) => (
|
|
// oxlint-disable-next-line react/no-array-index-key
|
|
<div key={i} className="flex flex-col gap-1">
|
|
<div className="flex items-center gap-2 my-1">
|
|
<span
|
|
className={classNames(
|
|
"font-mono text-editor select-auto cursor-auto",
|
|
cookie.isDeleted ? "line-through opacity-60 text-text-subtle" : "text-text",
|
|
)}
|
|
>
|
|
{cookie.name}
|
|
<span className="text-text-subtlest select-auto cursor-auto mx-0.5">=</span>
|
|
{cookie.value}
|
|
</span>
|
|
{cookie.isDeleted && (
|
|
<span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded">
|
|
Deleted
|
|
</span>
|
|
)}
|
|
</div>
|
|
<KeyValueRows>
|
|
{[
|
|
cookie.domain && (
|
|
<KeyValueRow labelColor="info" label="Domain" key="domain">
|
|
{cookie.domain}
|
|
</KeyValueRow>
|
|
),
|
|
cookie.path && (
|
|
<KeyValueRow labelColor="info" label="Path" key="path">
|
|
{cookie.path}
|
|
</KeyValueRow>
|
|
),
|
|
cookie.expires && (
|
|
<KeyValueRow labelColor="info" label="Expires" key="expires">
|
|
{cookie.expires}
|
|
</KeyValueRow>
|
|
),
|
|
cookie.maxAge && (
|
|
<KeyValueRow labelColor="info" label="Max-Age" key="maxAge">
|
|
{cookie.maxAge}
|
|
</KeyValueRow>
|
|
),
|
|
cookie.secure && (
|
|
<KeyValueRow labelColor="info" label="Secure" key="secure">
|
|
true
|
|
</KeyValueRow>
|
|
),
|
|
cookie.httpOnly && (
|
|
<KeyValueRow labelColor="info" label="HttpOnly" key="httpOnly">
|
|
true
|
|
</KeyValueRow>
|
|
),
|
|
cookie.sameSite && (
|
|
<KeyValueRow labelColor="info" label="SameSite" key="sameSite">
|
|
{cookie.sameSite}
|
|
</KeyValueRow>
|
|
),
|
|
].filter((item): item is JSX.Element => Boolean(item))}
|
|
</KeyValueRows>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</DetailsBanner>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NoCookies() {
|
|
return <span className="text-text-subtlest text-sm italic">No Cookies</span>;
|
|
}
|