Merge dev into main

This commit is contained in:
Linus Rath
2026-03-18 16:58:00 +01:00
3 changed files with 98 additions and 56 deletions
+77 -40
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useMemo, useRef, useCallback } from "react"; import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types";
@@ -817,7 +817,6 @@ export function EmailViewer({
const addTrustedSender = useSettingsStore((state) => state.addTrustedSender); const addTrustedSender = useSettingsStore((state) => state.addTrustedSender);
const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted); const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted);
const emailKeywords = useSettingsStore((state) => state.emailKeywords); const emailKeywords = useSettingsStore((state) => state.emailKeywords);
const debugMode = useSettingsStore((state) => state.debugMode);
const toolbarPosition = useSettingsStore((state) => state.toolbarPosition); const toolbarPosition = useSettingsStore((state) => state.toolbarPosition);
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels); const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled); const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
@@ -854,7 +853,7 @@ export function EmailViewer({
const tagMenuRef = useRef<HTMLDivElement>(null); const tagMenuRef = useRef<HTMLDivElement>(null);
const moveMenuRef = useRef<HTMLDivElement>(null); const moveMenuRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null); const toolbarRef = useRef<HTMLDivElement>(null);
const [overflowCount, setOverflowCount] = useState(0); const [hiddenPriorities, setHiddenPriorities] = useState<Set<number>>(new Set());
const currentColor = getCurrentColor(email?.keywords); const currentColor = getCurrentColor(email?.keywords);
// S/MIME state // S/MIME state
@@ -873,7 +872,7 @@ export function EmailViewer({
const [tnefAttachments, setTnefAttachments] = useState<TnefAttachment[]>([]); const [tnefAttachments, setTnefAttachments] = useState<TnefAttachment[]>([]);
// Ensure S/MIME key records are loaded from IndexedDB // Ensure S/MIME key records are loaded from IndexedDB
useEffect(() => { useLayoutEffect(() => {
smimeStore.load(); smimeStore.load();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -947,10 +946,17 @@ export function EmailViewer({
useEffect(() => { useEffect(() => {
const el = toolbarRef.current; const el = toolbarRef.current;
if (!el) return; if (!el) return;
let rafId: number | null = null;
const calculate = () => { const calculate = () => {
rafId = null;
const items = Array.from(el.querySelectorAll<HTMLElement>('[data-overflow-item]')); const items = Array.from(el.querySelectorAll<HTMLElement>('[data-overflow-item]'));
if (items.length === 0) return; if (items.length === 0) {
// Sort descending by priority so highest number (least important) is first setHiddenPriorities(prev => prev.size === 0 ? prev : new Set());
return;
}
// Sort descending by priority so highest number (least important) is hidden first
items.sort((a, b) => items.sort((a, b) =>
Number(b.dataset.overflowPriority || 0) - Number(a.dataset.overflowPriority || 0) Number(b.dataset.overflowPriority || 0) - Number(a.dataset.overflowPriority || 0)
); );
@@ -965,7 +971,7 @@ export function EmailViewer({
rightGroup.style.flexShrink = '0'; rightGroup.style.flexShrink = '0';
el.style.overflow = 'hidden'; el.style.overflow = 'hidden';
// Iteratively hide items until content fits // Iteratively hide items until content fits
let count = 0; const hidden = new Set<number>();
const isOverflowing = () => const isOverflowing = () =>
leftGroup.scrollWidth + rightGroup.scrollWidth + mainGap > containerWidth + 1; leftGroup.scrollWidth + rightGroup.scrollWidth + mainGap > containerWidth + 1;
for (const item of items) { for (const item of items) {
@@ -973,18 +979,54 @@ export function EmailViewer({
// Skip items already hidden by CSS (e.g., on mobile) // Skip items already hidden by CSS (e.g., on mobile)
if (item.offsetWidth === 0) continue; if (item.offsetWidth === 0) continue;
item.style.display = 'none'; item.style.display = 'none';
count++; hidden.add(Number(item.dataset.overflowPriority));
} }
// Restore layout // Restore layout
leftGroup.style.flexShrink = ''; leftGroup.style.flexShrink = '';
rightGroup.style.flexShrink = ''; rightGroup.style.flexShrink = '';
el.style.overflow = ''; el.style.overflow = '';
setOverflowCount(prev => prev === count ? prev : count); setHiddenPriorities(prev => {
if (prev.size === hidden.size && [...hidden].every(p => prev.has(p))) return prev;
return hidden;
});
}; };
const observer = new ResizeObserver(calculate);
observer.observe(el); const scheduleCalculate = () => {
return () => observer.disconnect(); if (rafId !== null) cancelAnimationFrame(rafId);
}, [toolbarPosition]); rafId = requestAnimationFrame(calculate);
};
// Recalculate on container resize
const resizeObserver = new ResizeObserver(scheduleCalculate);
resizeObserver.observe(el);
// Recalculate when children change (conditional items, label visibility)
const mutationObserver = new MutationObserver(scheduleCalculate);
mutationObserver.observe(el, { childList: true, subtree: true });
// Initial synchronous calculation to avoid flash
calculate();
return () => {
if (rafId !== null) cancelAnimationFrame(rafId);
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [
toolbarPosition,
email?.id,
showToolbarLabels,
isLoading,
moveTree.length,
colorOptions.length,
currentColor,
isInJunkFolder,
isTablet,
tabletListVisible,
onBack,
onMarkAsSpam,
onUndoSpam,
]);
// Contact sidebar state // Contact sidebar state
const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null); const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null);
@@ -1081,21 +1123,19 @@ export function EmailViewer({
if (!email || !client) return; if (!email || !client) return;
const smimeDebug = (...args: unknown[]) => { const smimeDebug = (...args: unknown[]) => {
if (debugMode) { if (useSettingsStore.getState().debugMode) {
console.debug(...args); console.debug(...args);
} }
}; };
const smimeWarn = (...args: unknown[]) => { const smimeWarn = (...args: unknown[]) => {
if (debugMode) { if (useSettingsStore.getState().debugMode) {
console.warn(...args); console.warn(...args);
} }
}; };
const smimeError = (...args: unknown[]) => { const smimeError = (...args: unknown[]) => {
if (debugMode) { console.error(...args);
console.error(...args);
}
}; };
const rawContentType = email.headers?.['content-type'] || email.headers?.['Content-Type']; const rawContentType = email.headers?.['content-type'] || email.headers?.['Content-Type'];
@@ -1336,7 +1376,7 @@ export function EmailViewer({
candidates: candidateSummaries, candidates: candidateSummaries,
}); });
if (debugMode && typeof window !== 'undefined') { if (useSettingsStore.getState().debugMode && typeof window !== 'undefined') {
const debugPayload = { const debugPayload = {
emailId: email!.id, emailId: email!.id,
detection, detection,
@@ -1616,7 +1656,6 @@ export function EmailViewer({
}, [ }, [
email, email,
client, client,
debugMode,
prepareSmimeUnlock, prepareSmimeUnlock,
smimeStore.autoImportSignerCerts, smimeStore.autoImportSignerCerts,
smimeStore.keyRecords, smimeStore.keyRecords,
@@ -1636,18 +1675,16 @@ export function EmailViewer({
debug.group('TNEF Processing'); debug.group('TNEF Processing');
debug.log('Found TNEF attachment:', tnefAtt.name, 'type:', tnefAtt.type, 'blobId:', tnefAtt.blobId, 'size:', tnefAtt.size); debug.log('Found TNEF attachment:', tnefAtt.name, 'type:', tnefAtt.type, 'blobId:', tnefAtt.blobId, 'size:', tnefAtt.size);
// Only process if the email has no usable HTML body // Check if the email already has a usable HTML body
const hasHtmlBody = !!( const hasHtmlBody = !!(
email.htmlBody?.[0]?.partId && email.htmlBody?.[0]?.partId &&
email.bodyValues?.[email.htmlBody[0].partId]?.value?.trim() email.bodyValues?.[email.htmlBody[0].partId]?.value?.trim()
); );
if (hasHtmlBody) { if (hasHtmlBody) {
debug.log('TNEF: Email already has HTML body, skipping TNEF extraction'); debug.log('TNEF: Email already has HTML body, will extract attachments only');
debug.log(' HTML partId:', email.htmlBody?.[0]?.partId, 'body length:', email.bodyValues?.[email.htmlBody![0].partId]?.value?.length); } else {
debug.groupEnd(); debug.log('TNEF: Email has no HTML body, proceeding with full TNEF extraction');
return;
} }
debug.log('TNEF: Email has no HTML body, proceeding with TNEF extraction');
let cancelled = false; let cancelled = false;
@@ -1682,10 +1719,10 @@ export function EmailViewer({
debug.log('TNEF parse result — htmlBody:', !!parsed.htmlBody, '(' + (parsed.htmlBody?.length ?? 0) + ' chars)', ', body:', !!parsed.body, '(' + (parsed.body?.length ?? 0) + ' chars)', ', attachments:', parsed.attachments.length); debug.log('TNEF parse result — htmlBody:', !!parsed.htmlBody, '(' + (parsed.htmlBody?.length ?? 0) + ' chars)', ', body:', !!parsed.body, '(' + (parsed.body?.length ?? 0) + ' chars)', ', attachments:', parsed.attachments.length);
if (parsed.htmlBody) { if (parsed.htmlBody && !hasHtmlBody) {
setTnefHtml(parsed.htmlBody); setTnefHtml(parsed.htmlBody);
} }
if (parsed.body) { if (parsed.body && !hasHtmlBody) {
setTnefText(parsed.body); setTnefText(parsed.body);
} }
if (parsed.attachments.length > 0) { if (parsed.attachments.length > 0) {
@@ -1790,8 +1827,8 @@ export function EmailViewer({
} }
const jmapAttachments = (email?.attachments ?? []) const jmapAttachments = (email?.attachments ?? [])
// Hide winmail.dat when we have successfully extracted TNEF content // Hide winmail.dat when we have successfully extracted TNEF content or attachments
.filter(att => !(tnefHtml || tnefText) || !isTnefAttachment(att.name, att.type)) .filter(att => !(tnefHtml || tnefText || tnefAttachments.length > 0) || !isTnefAttachment(att.name, att.type))
.map((attachment, index) => ({ .map((attachment, index) => ({
id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`, id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`,
name: attachment.name || null, name: attachment.name || null,
@@ -2737,7 +2774,7 @@ export function EmailViewer({
{/* Overflow: reply */} {/* Overflow: reply */}
<button <button
onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 10 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(1) ? "" : "sm:hidden")}
> >
<Reply className="w-4 h-4" /> <Reply className="w-4 h-4" />
{t('reply')} {t('reply')}
@@ -2745,7 +2782,7 @@ export function EmailViewer({
{/* Overflow: reply all */} {/* Overflow: reply all */}
<button <button
onClick={() => { onReplyAll?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onReplyAll?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 9 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(2) ? "" : "sm:hidden")}
> >
<ReplyAll className="w-4 h-4" /> <ReplyAll className="w-4 h-4" />
{t('reply_all')} {t('reply_all')}
@@ -2753,7 +2790,7 @@ export function EmailViewer({
{/* Overflow: forward */} {/* Overflow: forward */}
<button <button
onClick={() => { onForward?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onForward?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 8 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(3) ? "" : "sm:hidden")}
> >
<Forward className="w-4 h-4" /> <Forward className="w-4 h-4" />
{t('forward')} {t('forward')}
@@ -2761,14 +2798,14 @@ export function EmailViewer({
{/* Overflow: archive */} {/* Overflow: archive */}
<button <button
onClick={() => { onArchive?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onArchive?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 7 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(4) ? "" : "sm:hidden")}
> >
<Archive className="w-4 h-4" /> <Archive className="w-4 h-4" />
{t('archive')} {t('archive')}
</button> </button>
{/* Overflow: move to folder — submenu */} {/* Overflow: move to folder — submenu */}
{moveTree.length > 0 && onMoveToMailbox && ( {moveTree.length > 0 && onMoveToMailbox && (
<div className={cn("relative", overflowCount >= 6 ? "" : "sm:hidden")} <div className={cn("relative", hiddenPriorities.has(5) ? "" : "sm:hidden")}
onMouseEnter={() => setMoreMenuSub('move')} onMouseEnter={() => setMoreMenuSub('move')}
onMouseLeave={() => setMoreMenuSub(null)} onMouseLeave={() => setMoreMenuSub(null)}
> >
@@ -2820,7 +2857,7 @@ export function EmailViewer({
)} )}
{/* Overflow: tag — submenu */} {/* Overflow: tag — submenu */}
{colorOptions.length > 0 && ( {colorOptions.length > 0 && (
<div className={cn("relative", overflowCount >= 5 ? "" : "sm:hidden")} <div className={cn("relative", hiddenPriorities.has(6) ? "" : "sm:hidden")}
onMouseEnter={() => setMoreMenuSub('tag')} onMouseEnter={() => setMoreMenuSub('tag')}
onMouseLeave={() => setMoreMenuSub(null)} onMouseLeave={() => setMoreMenuSub(null)}
> >
@@ -2868,7 +2905,7 @@ export function EmailViewer({
{(onMarkAsSpam || onUndoSpam) && ( {(onMarkAsSpam || onUndoSpam) && (
<button <button
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 4 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(7) ? "" : "sm:hidden")}
> >
{isInJunkFolder ? ( {isInJunkFolder ? (
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" /> <ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
@@ -2881,7 +2918,7 @@ export function EmailViewer({
{/* Overflow: toggle read */} {/* Overflow: toggle read */}
<button <button
onClick={() => { onMarkAsRead?.(email.id, isUnread); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onMarkAsRead?.(email.id, isUnread); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 3 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(8) ? "" : "sm:hidden")}
> >
{isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />} {isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />}
{isUnread ? t('mark_read') : t('mark_unread')} {isUnread ? t('mark_read') : t('mark_unread')}
@@ -2889,7 +2926,7 @@ export function EmailViewer({
{/* Overflow: print */} {/* Overflow: print */}
<button <button
onClick={() => { handlePrint(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { handlePrint(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 2 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(9) ? "" : "sm:hidden")}
> >
<Printer className="w-4 h-4" /> <Printer className="w-4 h-4" />
{t('print')} {t('print')}
@@ -2897,7 +2934,7 @@ export function EmailViewer({
{/* Overflow: view source */} {/* Overflow: view source */}
<button <button
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 1 ? "" : "sm:hidden")} className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(10) ? "" : "sm:hidden")}
> >
<Code className="w-4 h-4" /> <Code className="w-4 h-4" />
{t('view_source')} {t('view_source')}
+16 -15
View File
@@ -13,7 +13,7 @@
"asn1js": "^3.0.7", "asn1js": "^3.0.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.3.1", "dompurify": "^3.3.3",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"next": "^16.1.5", "next": "^16.1.5",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
@@ -4731,9 +4731,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.3.1", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
@@ -4762,17 +4762,18 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/elliptic": { "node_modules/elliptic": {
"version": "6.5.0", "version": "6.6.1",
"resolved": "git+ssh://git@github.com/mahrud/elliptic.git#75637c76678e83c31682fd967c2fa9ff4761b3fc", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bn.js": "^4.4.0", "bn.js": "^4.11.9",
"brorand": "^1.0.1", "brorand": "^1.1.0",
"hash.js": "^1.0.0", "hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0", "hmac-drbg": "^1.0.1",
"inherits": "^2.0.1", "inherits": "^2.0.4",
"minimalistic-assert": "^1.0.0", "minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.0" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
@@ -8656,9 +8657,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.22.0", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
+5 -1
View File
@@ -36,7 +36,7 @@
"asn1js": "^3.0.7", "asn1js": "^3.0.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.3.1", "dompurify": "^3.3.3",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"next": "^16.1.5", "next": "^16.1.5",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
@@ -74,5 +74,9 @@
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.16" "vitest": "^4.0.16"
},
"overrides": {
"elliptic": "^6.6.1",
"undici": "^7.24.0"
} }
} }