Commit 33fd6e80 by luoqi

Merge session scope isolation into test

parents a9248e61 a5fd5abb
Pipeline #3864 failed in 0 seconds
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-config-next": "^16.2.4", "eslint-config-next": "^16.2.4",
"jsdom": "^26.1.0",
"tailwindcss": "^4.2.4", "tailwindcss": "^4.2.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^3.2.7" "vitest": "^3.2.7"
......
...@@ -1174,6 +1174,16 @@ export function AssistantChat({ ...@@ -1174,6 +1174,16 @@ export function AssistantChat({
finalized: string; finalized: string;
} | null>(null); } | null>(null);
useEffect(() => () => {
const d = dictRef.current;
dictRef.current = null;
if (d) {
d.socket.disconnect();
void d.ctx.close().catch(() => undefined);
d.stream.getTracks().forEach((t) => t.stop());
}
}, []);
const stopVoice = () => { const stopVoice = () => {
const d = dictRef.current; const d = dictRef.current;
dictRef.current = null; dictRef.current = null;
...@@ -1191,7 +1201,7 @@ export function AssistantChat({ ...@@ -1191,7 +1201,7 @@ export function AssistantChat({
stopVoice(); stopVoice();
return; return;
} }
const token = useAuthStore.getState().accessToken; const { accessToken: token, sessionVersion } = useAuthStore.getState();
if (!token) { if (!token) {
setVoiceErr('未鉴权,请重新登录'); setVoiceErr('未鉴权,请重新登录');
return; return;
...@@ -1200,6 +1210,10 @@ export function AssistantChat({ ...@@ -1200,6 +1210,10 @@ export function AssistantChat({
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
}); });
if (useAuthStore.getState().sessionVersion !== sessionVersion) {
stream.getTracks().forEach((t) => t.stop());
return;
}
const socket = io(`${env.apiBaseUrl}/pac/v1/assistant/dictation`, { const socket = io(`${env.apiBaseUrl}/pac/v1/assistant/dictation`, {
query: { token }, query: { token },
transports: ['websocket'], transports: ['websocket'],
......
'use client'; 'use client';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import type { import type {
AssignmentProposal, AssignmentProposal,
RearrangeDraft, RearrangeDraft,
...@@ -135,6 +135,8 @@ export function useAssistantChat() { ...@@ -135,6 +135,8 @@ export function useAssistantChat() {
*/ */
const [model, setModel] = useState('deepseek'); const [model, setModel] = useState('deepseek');
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
// 页面/会话卸载后停止旧流,避免旧响应继续处理工具结果。
useEffect(() => () => abortRef.current?.abort(), []);
/** /**
* 消息流的**同步镜像** —— 给那些"要先读当前状态、再发请求"的回调用。 * 消息流的**同步镜像** —— 给那些"要先读当前状态、再发请求"的回调用。
* ⛔ 别把这类读取塞进 `setMessages` 的 updater:StrictMode 会把 updater 跑两次, * ⛔ 别把这类读取塞进 `setMessages` 的 updater:StrictMode 会把 updater 跑两次,
......
// @vitest-environment jsdom
import { act, useEffect, useState } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { AuthGate } from './auth-gate';
import { useAuthStore } from '@/stores/auth-store';
import { api } from '@/lib/api-client';
vi.mock('@/components/mock-login-dialog', () => ({ MockLoginDialog: () => <div>LOGIN</div> }));
vi.mock('@/components/session-expired', () => ({ SessionExpired: () => <div>REENTER HOST</div> }));
vi.mock('@/lib/embed', () => ({ isEmbedded: () => true }));
const deferred = () => {
let resolve!: (v: Response) => void;
const promise = new Promise<Response>((r) => {
resolve = r;
});
return { promise, resolve };
};
const tokens = (clinic: string) => ({
accessToken: `x.${btoa(JSON.stringify({ sub: 'admin', hostId: 'friday', tenantId: 'market', role: 'leader', permissions: [], orgScope: [clinic] }))}.x`,
refreshToken: clinic,
expiresIn: 3600,
});
const result = (data: unknown, code = 0) =>
new Response(JSON.stringify({ code, data }), { headers: { 'Content-Type': 'application/json' } });
function TestPage() {
const clinic = useAuthStore((s) => s.user?.clinicIds?.[0]);
const [data, setData] = useState('loading');
useEffect(() => {
void api
.get<string>('/business')
.then(setData)
.catch(() => {});
}, []);
return (
<div>
clinic:{clinic};data:{data}
</div>
);
}
let el: HTMLDivElement, root: Root;
let seen: string[], businessCalls: string[], oldSession: ReturnType<typeof deferred>;
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
useAuthStore.getState().clear();
window.localStorage.clear();
window.history.replaceState({}, '', '/plans?code=youen');
el = document.createElement('div');
document.body.appendChild(el);
root = createRoot(el);
seen = [];
businessCalls = [];
oldSession = deferred();
vi.stubGlobal(
'fetch',
vi.fn(async (url: URL, opts?: RequestInit) => {
const path = new URL(url).pathname;
const auth = new Headers(opts?.headers).get('Authorization');
const clinic = auth ? JSON.parse(atob(auth.split('.')[1]!)).orgScope[0] : '';
if (path.endsWith('/client-diag')) return result(null);
if (path.endsWith('/exchange-code')) return result(tokens('youen'));
if (path.endsWith('/session')) {
if (clinic === 'puer') return oldSession.promise;
return result({
clinicIds: [clinic],
sourceUnits: [clinic],
permissions: [],
dictionary: { clinics: { [clinic]: clinic } },
});
}
if (path === '/business') {
businessCalls.push(clinic);
return result(clinic);
}
throw new Error(path);
}),
);
});
afterEach(async () => {
await act(async () => root.unmount());
el.remove();
vi.unstubAllGlobals();
});
async function settle() {
await act(async () => {
await new Promise((r) => setTimeout(r, 5));
});
seen.push(el.textContent ?? '');
}
it('never mounts old clinic business while exchanging host code, even if old session arrives last', async () => {
useAuthStore.getState().setTokens(tokens('puer'));
await new Promise((r) => setTimeout(r, 0));
await act(async () =>
root.render(
<AuthGate>
<TestPage />
</AuthGate>,
),
);
await settle();
await settle();
expect(el.textContent).toContain('clinic:youen;data:youen');
await act(async () =>
oldSession.resolve(result({ clinicIds: ['puer'], sourceUnits: ['puer'], permissions: [] })),
);
await settle();
expect(el.textContent).toContain('clinic:youen;data:youen');
expect(businessCalls).toEqual(['youen']);
expect(seen.join('')).not.toContain('puer');
});
it('does not fall back to a stored clinic when the new host code fails', async () => {
useAuthStore.getState().setTokens(tokens('puer'));
const normal = vi.mocked(fetch).getMockImplementation()!;
vi.mocked(fetch).mockImplementation((url, opts) =>
new URL(String(url)).pathname.endsWith('/exchange-code')
? Promise.resolve(result(null, 10102))
: normal(url, opts),
);
await act(async () =>
root.render(
<AuthGate>
<TestPage />
</AuthGate>,
),
);
await settle();
expect(el.textContent).toContain('REENTER HOST');
expect(businessCalls).toEqual([]);
expect(useAuthStore.getState().accessToken).toBeNull();
});
it('remounts all protected page state on a scope change for the same user', async () => {
await act(async () =>
root.render(
<AuthGate>
<TestPage />
</AuthGate>,
),
);
await settle();
expect(el.textContent).toContain('youen');
// 新范围独立返回,模拟同账号切到普尔。
const normal = vi.mocked(fetch).getMockImplementation()!;
vi.mocked(fetch).mockImplementation((url, opts) =>
new URL(String(url)).pathname.endsWith('/session')
? Promise.resolve(result({ clinicIds: ['puer'], sourceUnits: ['puer'], permissions: [] }))
: normal(url, opts),
);
await act(async () => useAuthStore.getState().setTokens(tokens('puer')));
await settle();
expect(el.textContent).toContain('clinic:puer;data:puer');
expect(businessCalls).toEqual(['youen', 'puer']);
});
it('reloads without a code using only the current saved session', async () => {
window.history.replaceState({}, '', '/supervisor');
useAuthStore.getState().setTokens(tokens('youen'));
await act(async () => root.render(<AuthGate><TestPage /></AuthGate>));
await settle();
expect(el.textContent).toContain('clinic:youen;data:youen');
expect(businessCalls).toEqual(['youen']);
expect(vi.mocked(fetch).mock.calls.some(([url]) => String(url).includes('exchange-code'))).toBe(false);
});
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { useAuthBootstrap } from '@/hooks/use-auth-bootstrap'; import { useAuthBootstrap } from '@/hooks/use-auth-bootstrap';
import { useAuthStore } from '@/stores/auth-store'; import { clinicScopeReady, useAuthStore } from '@/stores/auth-store';
import { MockLoginDialog } from '@/components/mock-login-dialog'; import { MockLoginDialog } from '@/components/mock-login-dialog';
import { SessionExpired } from '@/components/session-expired'; import { SessionExpired } from '@/components/session-expired';
import { isEmbedded } from '@/lib/embed'; import { isEmbedded } from '@/lib/embed';
...@@ -16,6 +16,9 @@ const Placeholder = () => ( ...@@ -16,6 +16,9 @@ const Placeholder = () => (
export function AuthGate({ children }: { children: React.ReactNode }) { export function AuthGate({ children }: { children: React.ReactNode }) {
const { status } = useAuthBootstrap(); const { status } = useAuthBootstrap();
const sessionVersion = useAuthStore((s) => s.sessionVersion);
const sessionError = useAuthStore((s) => s.sessionError);
const scopeReady = useAuthStore((s) => clinicScopeReady(s.user));
const isAuthenticated = useAuthStore((s) => s.isAuthenticated()); const isAuthenticated = useAuthStore((s) => s.isAuthenticated());
// SSR + first client render 必须同形态(否则 hydration mismatch)。 // SSR + first client render 必须同形态(否则 hydration mismatch)。
...@@ -65,12 +68,11 @@ export function AuthGate({ children }: { children: React.ReactNode }) { ...@@ -65,12 +68,11 @@ export function AuthGate({ children }: { children: React.ReactNode }) {
if (!mounted) return <Placeholder />; if (!mounted) return <Placeholder />;
// mount 后真正分流: // 新票和诊所范围就绪前不展示任何业务页面;换身份时卸载全部旧页面状态。
// - store 已 authenticated → 放行(覆盖 bootstrap 边界 case,如 back 重挂)
// - 还在 bootstrap → 占位
// - 都不是 → 弹"快速登录"对话框(开发 / 试部署用,后端 env 门控)
if (isAuthenticated) return <>{children}</>;
if (status === 'authenticating' || status === 'idle') return <Placeholder />; if (status === 'authenticating' || status === 'idle') return <Placeholder />;
if (sessionError) return <SessionExpired />;
if (isAuthenticated && !scopeReady) return <Placeholder />;
if (isAuthenticated) return <Fragment key={sessionVersion}>{children}</Fragment>;
// 嵌入态(宿主 iframe):不弹 dev 快速登录 —— 会话失效只能回宿主重进,给友好引导 + 重试。 // 嵌入态(宿主 iframe):不弹 dev 快速登录 —— 会话失效只能回宿主重进,给友好引导 + 重试。
if (isEmbedded()) return <SessionExpired />; if (isEmbedded()) return <SessionExpired />;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { useEffect, useId, useRef, useState } from 'react'; import { useEffect, useId, useRef, useState } from 'react';
import { api } from '@/lib/api-client'; import { api } from '@/lib/api-client';
import Link from 'next/link'; import Link from 'next/link';
import { useAuthStore } from '@/stores/auth-store'; import { sessionIdentity, useAuthStore } from '@/stores/auth-store';
import { CircleAlert, Loader2, RefreshCw } from 'lucide-react'; import { CircleAlert, Loader2, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
...@@ -59,7 +59,7 @@ export function HostAppointmentDialog({ ...@@ -59,7 +59,7 @@ export function HostAppointmentDialog({
const returnFocusRef = useRef<HTMLElement | null>( const returnFocusRef = useRef<HTMLElement | null>(
typeof document === 'undefined' ? null : (document.activeElement as HTMLElement), typeof document === 'undefined' ? null : (document.activeElement as HTMLElement),
); );
const actor = useAuthStore((s) => s.user?.sub ?? 'guest'); const actor = useAuthStore((s) => sessionIdentity(s.user));
const storageKey = `pac:host-appointment:${actor}:${planId ?? patientId}`; const storageKey = `pac:host-appointment:${actor}:${planId ?? patientId}`;
const [context, setContext] = useState<AppointmentContext | null>(null); const [context, setContext] = useState<AppointmentContext | null>(null);
const [clinic, setClinic] = useState(targetOrgRef ?? ''); const [clinic, setClinic] = useState(targetOrgRef ?? '');
......
'use client'; 'use client';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { ApiCode } from '@pac/types'; import { ApiCode } from '@pac/types';
import { env } from '@/lib/env'; import { env } from '@/lib/env';
import { tryRefresh } from '@/lib/api-client'; import { tryRefresh } from '@/lib/api-client';
...@@ -90,6 +90,8 @@ const DURATION_FALLBACK: Record<FixedSectionId, string> = { ...@@ -90,6 +90,8 @@ const DURATION_FALLBACK: Record<FixedSectionId, string> = {
export function useScriptStream(): UseScriptStream { export function useScriptStream(): UseScriptStream {
const [state, setState] = useState<ScriptStreamState>({ status: 'idle' }); const [state, setState] = useState<ScriptStreamState>({ status: 'idle' });
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
// 页面/会话卸载后停止旧流,避免旧响应继续处理工具结果。
useEffect(() => () => abortRef.current?.abort(), []);
const reset = useCallback(() => setState({ status: 'idle' }), []); const reset = useCallback(() => setState({ status: 'idle' }), []);
......
'use client'; 'use client';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { ApiCode } from '@pac/types'; import { ApiCode } from '@pac/types';
import { env } from '@/lib/env'; import { env } from '@/lib/env';
import { tryRefresh } from '@/lib/api-client'; import { tryRefresh } from '@/lib/api-client';
...@@ -51,6 +51,8 @@ export interface UseSummaryStream { ...@@ -51,6 +51,8 @@ export interface UseSummaryStream {
export function useSummaryStream(): UseSummaryStream { export function useSummaryStream(): UseSummaryStream {
const [state, setState] = useState<SummaryStreamState>({ status: 'idle' }); const [state, setState] = useState<SummaryStreamState>({ status: 'idle' });
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
// 页面/会话卸载后停止旧流,避免旧响应继续处理工具结果。
useEffect(() => () => abortRef.current?.abort(), []);
const reset = useCallback(() => setState({ status: 'idle' }), []); const reset = useCallback(() => setState({ status: 'idle' }), []);
const abort = useCallback(() => { const abort = useCallback(() => {
......
'use client'; 'use client';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { io, type Socket } from 'socket.io-client'; import { io, type Socket } from 'socket.io-client';
import { env } from '@/lib/env'; import { env } from '@/lib/env';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
...@@ -39,6 +39,8 @@ export function useRealtimeCoach(planId: string) { ...@@ -39,6 +39,8 @@ export function useRealtimeCoach(planId: string) {
analyserRef.current = null; analyserRef.current = null;
}, []); }, []);
useEffect(() => cleanup, [cleanup]);
const start = useCallback(async (provider: 'qwen' | 'gemini' = 'qwen') => { const start = useCallback(async (provider: 'qwen' | 'gemini' = 'qwen') => {
const token = useAuthStore.getState().accessToken; const token = useAuthStore.getState().accessToken;
if (!token) { if (!token) {
...@@ -98,6 +100,10 @@ export function useRealtimeCoach(planId: string) { ...@@ -98,6 +100,10 @@ export function useRealtimeCoach(planId: string) {
// 开 WebRTC 降噪/回声消除/自动增益,过滤稳态背景噪音 // 开 WebRTC 降噪/回声消除/自动增益,过滤稳态背景噪音
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
}); });
if (socketRef.current !== socket) {
stream.getTracks().forEach((t) => t.stop());
return;
}
streamRef.current = stream; streamRef.current = stream;
const ctx = new AudioContext(); const ctx = new AudioContext();
ctxRef.current = ctx; ctxRef.current = ctx;
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Clock, RefreshCw } from 'lucide-react'; import { Clock, RefreshCw } from 'lucide-react';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { refreshToken as apiRefresh } from '@/lib/auth-api'; import { tryRefresh } from '@/lib/api-client';
/** /**
* 嵌入态(宿主 iframe)会话失效兜底 —— 替代 dev 的 MockLoginDialog。 * 嵌入态(宿主 iframe)会话失效兜底 —— 替代 dev 的 MockLoginDialog。
...@@ -24,8 +24,7 @@ export function SessionExpired() { ...@@ -24,8 +24,7 @@ export function SessionExpired() {
// 有 refresh token → 试着静默续期(挡暂时性失败);没有就只能回宿主,直接标记失败。 // 有 refresh token → 试着静默续期(挡暂时性失败);没有就只能回宿主,直接标记失败。
if (rt) { if (rt) {
try { try {
const r = await apiRefresh(rt); if (!(await tryRefresh())) throw new Error('Refresh failed');
useAuthStore.getState().setTokens(r);
return; // 成功:store 更新 → AuthGate 自动放行,本组件卸载 return; // 成功:store 更新 → AuthGate 自动放行,本组件卸载
} catch { } catch {
/* fall through */ /* fall through */
......
...@@ -165,16 +165,20 @@ export function useAuthBootstrap() { ...@@ -165,16 +165,20 @@ export function useAuthBootstrap() {
// ─── 1. URL 有 code → 优先(新人接管,覆盖旧 token)─── // ─── 1. URL 有 code → 优先(新人接管,覆盖旧 token)───
if (code) { if (code) {
setStatus('authenticating'); setStatus('authenticating');
// 宿主新票代表一次新的登录;不能借用浏览器里另一诊所的身份兜底。
clear();
const loginVersion = useAuthStore.getState().sessionVersion;
try { try {
const r = await exchangeCode(code); const r = await exchangeCode(code);
if (useAuthStore.getState().sessionVersion !== loginVersion) return;
setTokens(r); setTokens(r);
// ⚠️ 地址栏上面已经剥干净了 —— ⛔ 别在这里再 replaceState 一次(那正是卡死的成因) // ⚠️ 地址栏上面已经剥干净了 —— ⛔ 别在这里再 replaceState 一次(那正是卡死的成因)
reportDiag({ ev: 'code-ok', codeTag }); reportDiag({ ev: 'code-ok', codeTag });
setStatus('ready'); setStatus('ready');
return; return;
} catch (e) { } catch (e) {
// code 失效(过期 / 重放) — 不一定要崩,localStorage 可能还有效(地址栏已剥,同上) if (useAuthStore.getState().sessionVersion !== loginVersion) return;
// ⭐ 关键证据:换 code 失败瞬间,store 里到底有没有 token(有→能兜住;无→必弹登录已过期) // 新票失败必须回宿主重进,⛔ 不能回落到旧会话。
reportDiag({ reportDiag({
ev: 'code-fail', ev: 'code-fail',
codeTag, codeTag,
...@@ -182,7 +186,8 @@ export function useAuthBootstrap() { ...@@ -182,7 +186,8 @@ export function useAuthBootstrap() {
...storeSnap(), ...storeSnap(),
...envInfo, ...envInfo,
}); });
// fall through 试 store setStatus('error');
return;
} }
} }
...@@ -244,13 +249,13 @@ export function useAuthBootstrap() { ...@@ -244,13 +249,13 @@ export function useAuthBootstrap() {
// → 10103 revoked → 误清会话 =「用着用着登录已过期」的竞态根因。 // → 10103 revoked → 误清会话 =「用着用着登录已过期」的竞态根因。
// tryRefresh 内部成功即 setTokens、失败即 clear + 发运行期掉线信标,故此处不再手动处理。 // tryRefresh 内部成功即 setTokens、失败即 clear + 发运行期掉线信标,故此处不再手动处理。
useEffect(() => { useEffect(() => {
if (!expiresAt || !refresh) return; if (status !== 'ready' || !expiresAt || !refresh) return;
const delay = Math.max(0, expiresAt - Date.now() - REFRESH_LEAD_MS); const delay = Math.max(0, expiresAt - Date.now() - REFRESH_LEAD_MS);
const timer = setTimeout(() => { const timer = setTimeout(() => {
void tryRefresh(); void tryRefresh();
}, delay); }, delay);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [expiresAt, refresh]); }, [status, expiresAt, refresh]);
return { status }; return { status };
} }
// @vitest-environment jsdom
import { beforeEach, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/stores/auth-store';
import { api, tryRefresh } from './api-client';
vi.mock('@/lib/auth-api', () => ({ getSession: () => new Promise(() => {}) }));
vi.mock('@/lib/auth-diag', () => ({
reportAuthDiag: vi.fn(),
authStoreSnap: () => ({}),
authEnvSnap: () => ({}),
}));
const deferred = () => {
let resolve!: (value: any) => void;
const promise = new Promise<any>((r) => {
resolve = r;
});
return { promise, resolve };
};
const tokens = (clinic: string) => ({
accessToken: `x.${Buffer.from(JSON.stringify({ sub: 'admin', hostId: 'friday', tenantId: 'market', orgScope: [clinic] })).toString('base64url')}.x`,
refreshToken: clinic,
expiresIn: 3600,
});
const result = (code: number, data?: any) =>
new Response(JSON.stringify({ code, data }), { headers: { 'Content-Type': 'application/json' } });
beforeEach(() => {
useAuthStore.getState().clear();
vi.stubGlobal('fetch', vi.fn());
});
it.each([0, 10106, 10107])(
'rejects stale response code %s without clearing or retrying under a new login',
async (code) => {
const pending = deferred();
vi.mocked(fetch).mockReturnValueOnce(pending.promise);
useAuthStore.getState().setTokens(tokens('puer'));
const response = api.get('/test');
const checked = expect(response).rejects.toMatchObject({ name: 'AbortError' });
useAuthStore.getState().setTokens(tokens('youen'));
pending.resolve(result(code, { clinic: 'puer' }));
await checked;
expect(useAuthStore.getState().refreshToken).toBe('youen');
expect(fetch).toHaveBeenCalledTimes(1);
},
);
it.each([0, 10103])('ignores late refresh result %s from previous login', async (code) => {
const pending = deferred();
vi.mocked(fetch).mockReturnValueOnce(pending.promise);
useAuthStore.getState().setTokens(tokens('puer'));
const p = tryRefresh();
useAuthStore.getState().setTokens(tokens('youen'));
pending.resolve(result(code, tokens('puer')));
expect(await p).toBe(false);
expect(useAuthStore.getState().refreshToken).toBe('youen');
});
it('single-flights renewal within a session but not across logins', async () => {
const old = deferred(),
fresh = deferred();
vi.mocked(fetch).mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise);
useAuthStore.getState().setTokens(tokens('puer'));
const a = tryRefresh(),
b = tryRefresh();
expect(fetch).toHaveBeenCalledTimes(1);
useAuthStore.getState().setTokens(tokens('youen'));
const c = tryRefresh();
expect(fetch).toHaveBeenCalledTimes(2);
old.resolve(result(10103));
await Promise.all([a, b]);
fresh.resolve(result(0, tokens('youen')));
expect(await c).toBe(true);
expect(useAuthStore.getState().refreshToken).toBe('youen');
});
...@@ -52,6 +52,12 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> { ...@@ -52,6 +52,12 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
} }
} }
const sessionVersion = useAuthStore.getState().sessionVersion;
const assertCurrentSession = () => {
if (auth && useAuthStore.getState().sessionVersion !== sessionVersion) {
throw new DOMException('Session changed', 'AbortError');
}
};
const finalHeaders = new Headers(headers); const finalHeaders = new Headers(headers);
if (body !== undefined && !(body instanceof FormData)) { if (body !== undefined && !(body instanceof FormData)) {
finalHeaders.set('Content-Type', 'application/json'); finalHeaders.set('Content-Type', 'application/json');
...@@ -67,6 +73,8 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> { ...@@ -67,6 +73,8 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body), body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
}); });
assertCurrentSession();
// True server crash — preserve HTTP semantics here so monitoring still works. // True server crash — preserve HTTP semantics here so monitoring still works.
if (res.status >= 500) { if (res.status >= 500) {
throw new ApiError(ApiCode.INTERNAL_ERROR, `Server error ${res.status}`); throw new ApiError(ApiCode.INTERNAL_ERROR, `Server error ${res.status}`);
...@@ -80,6 +88,8 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> { ...@@ -80,6 +88,8 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
throw new ApiError(ApiCode.INTERNAL_ERROR, `Malformed response (HTTP ${res.status})`); throw new ApiError(ApiCode.INTERNAL_ERROR, `Malformed response (HTTP ${res.status})`);
} }
assertCurrentSession();
if (payload.code === ApiCode.OK) { if (payload.code === ApiCode.OK) {
return payload.data as T; return payload.data as T;
} }
...@@ -94,6 +104,7 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> { ...@@ -94,6 +104,7 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
useAuthStore.getState().canRefresh() useAuthStore.getState().canRefresh()
) { ) {
const refreshed = await tryRefresh(); const refreshed = await tryRefresh();
assertCurrentSession();
if (refreshed) { if (refreshed) {
return request<T>(path, { ...opts, _retried: true }); return request<T>(path, { ...opts, _retried: true });
} }
...@@ -130,24 +141,27 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> { ...@@ -130,24 +141,27 @@ async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
// Silent refresh — 单飞模式(并发请求只调一次 /auth/refresh) // Silent refresh — 单飞模式(并发请求只调一次 /auth/refresh)
// ───────────────────────────────────────────────────── // ─────────────────────────────────────────────────────
let refreshInflight: Promise<boolean> | null = null; // 按会话与 refresh token 单飞,旧请求完成不能覆盖或清除新登录。
let refreshInflight: { token: string; version: number; promise: Promise<boolean> } | null = null;
/**
* 调用 /auth/refresh。成功 → 写 store 返回 true;失败 → 清 store 返回 false。
* 多请求同时撞 401 时,只调一次 refresh,其它请求 await 同一个 promise。
*/
export async function tryRefresh(): Promise<boolean> { export async function tryRefresh(): Promise<boolean> {
if (refreshInflight) return refreshInflight; const { refreshToken: token, sessionVersion: version } = useAuthStore.getState();
refreshInflight = doRefresh().finally(() => { if (!token) return false;
refreshInflight = null; if (refreshInflight?.token === token && refreshInflight.version === version) return refreshInflight.promise;
const flight = { token, version, promise: doRefresh() };
refreshInflight = flight;
void flight.promise.finally(() => {
if (refreshInflight === flight) refreshInflight = null;
}); });
return refreshInflight; return flight.promise;
} }
async function doRefresh(): Promise<boolean> { async function doRefresh(): Promise<boolean> {
const store = useAuthStore.getState(); const store = useAuthStore.getState();
const rt = store.refreshToken; const rt = store.refreshToken;
if (!rt) return false; if (!rt) return false;
const current = () => useAuthStore.getState().sessionVersion === store.sessionVersion
&& useAuthStore.getState().refreshToken === rt;
// 不复用 request(),避免循环:用裸 fetch 调 public 端点 // 不复用 request(),避免循环:用裸 fetch 调 public 端点
try { try {
...@@ -166,6 +180,7 @@ async function doRefresh(): Promise<boolean> { ...@@ -166,6 +180,7 @@ async function doRefresh(): Promise<boolean> {
refreshToken: string; refreshToken: string;
expiresIn: number; expiresIn: number;
}>; }>;
if (!current()) return false;
if (payload.code === ApiCode.OK && payload.data) { if (payload.code === ApiCode.OK && payload.data) {
store.setTokens({ store.setTokens({
accessToken: payload.data.accessToken, accessToken: payload.data.accessToken,
......
...@@ -24,7 +24,10 @@ import { STREAM_TOTAL_MS } from '@/components/assistant/cohort-stream'; ...@@ -24,7 +24,10 @@ import { STREAM_TOTAL_MS } from '@/components/assistant/cohort-stream';
* 听写状态全靠这个保留)。store 化之后**必须保持这个特性** —— * 听写状态全靠这个保留)。store 化之后**必须保持这个特性** ——
* 若顺手改成条件渲染,主管每收一次助手就丢一次对话,而这在代码上看不出任何异常。 * 若顺手改成条件渲染,主管每收一次助手就丢一次对话,而这在代码上看不出任何异常。
*/ */
let handoffTimer: ReturnType<typeof setTimeout> | undefined;
interface AssistantState { interface AssistantState {
resetSession: () => void;
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
/** /**
...@@ -87,6 +90,11 @@ interface AssistantState { ...@@ -87,6 +90,11 @@ interface AssistantState {
} }
export const useAssistantStore = create<AssistantState>((set, get) => ({ export const useAssistantStore = create<AssistantState>((set, get) => ({
resetSession: () => {
clearTimeout(handoffTimer);
handoffTimer = undefined;
set({ open: false, maximized: false, pending: null, activeClinicId: null, sheetSnapshot: null });
},
open: false, open: false,
setOpen: (open) => set({ open }), setOpen: (open) => set({ open }),
maximized: false, maximized: false,
...@@ -108,7 +116,8 @@ export const useAssistantStore = create<AssistantState>((set, get) => ({ ...@@ -108,7 +116,8 @@ export const useAssistantStore = create<AssistantState>((set, get) => ({
* 等于把他刚看完的工作台整个盖掉 —— 而他多半还要回去点下一格。 * 等于把他刚看完的工作台整个盖掉 —— 而他多半还要回去点下一格。
* ⛔ 别改回 true:要看大图,助手窗口自己有最大化按钮,那是**他**的决定。 * ⛔ 别改回 true:要看大图,助手窗口自己有最大化按钮,那是**他**的决定。
*/ */
window.setTimeout(() => get().ask(text), STREAM_TOTAL_MS); clearTimeout(handoffTimer);
handoffTimer = setTimeout(() => get().ask(text), STREAM_TOTAL_MS);
}, },
activeClinicId: null, activeClinicId: null,
setActiveClinicId: (activeClinicId) => set({ activeClinicId }), setActiveClinicId: (activeClinicId) => set({ activeClinicId }),
......
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore, sessionIdentity } from './auth-store';
import { useAssistantStore } from './assistant-store';
import { usePlanSyncStore } from './plan-sync-store';
const { getSession } = vi.hoisted(() => ({ getSession: vi.fn() }));
vi.mock('@/lib/auth-api', () => ({ getSession }));
const tick = () => new Promise((r) => setTimeout(r, 0));
const deferred = () => {
let resolve!: (value: any) => void;
const promise = new Promise<any>((r) => {
resolve = r;
});
return { promise, resolve };
};
const user = (clinic = 'puer', sub = 'admin') => ({
sub,
hostId: 'friday',
tenantId: 'market',
role: 'leader' as const,
orgScope: [clinic],
permissions: [],
});
const tokens = (u: object, suffix = '') => ({
accessToken: `x.${Buffer.from(JSON.stringify(u)).toString('base64url')}.${suffix}`,
refreshToken: `refresh-${suffix}`,
expiresIn: 3600,
});
const session = (clinic: string) => ({
clinicIds: [clinic],
sourceUnits: [clinic],
permissions: [],
dictionary: { clinics: { [clinic]: clinic } },
});
beforeEach(() => {
useAuthStore.getState().clear();
getSession.mockReset();
getSession.mockImplementation(() => new Promise(() => {}));
});
describe('session isolation', () => {
it('discards a previous login session response even when it arrives last', async () => {
const old = deferred(),
fresh = deferred();
getSession.mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise);
useAuthStore.getState().setTokens(tokens(user('puer', 'old')));
await tick();
useAuthStore.getState().setTokens(tokens(user('youen', 'new')));
await tick();
fresh.resolve(session('youen'));
await tick();
old.resolve(session('puer'));
await tick();
expect(useAuthStore.getState().user?.clinicIds).toEqual(['youen']);
expect(useAuthStore.getState().user?.dictionary?.clinics).toEqual({ youen: 'youen' });
});
it.each(['clinic', 'tenant', 'host', 'role', 'user'])(
'clears expanded scope when %s changes',
(field) => {
const previous = user();
useAuthStore.setState({ user: { ...previous, ...session('puer') } as any });
const next = { ...previous };
if (field === 'clinic') next.orgScope = ['youen'];
if (field === 'tenant') next.tenantId = 'another';
if (field === 'host') next.hostId = 'another';
if (field === 'role') (next as any).role = 'staff';
if (field === 'user') next.sub = 'another';
useAuthStore.getState().setTokens(tokens(next));
expect(useAuthStore.getState().user?.clinicIds).toBeUndefined();
expect(useAuthStore.getState().user?.sourceUnits).toBeUndefined();
},
);
it('preserves scope during ordinary token renewal', () => {
const u = { ...user(), ...session('puer') };
useAuthStore.setState({ user: u as any });
const version = useAuthStore.getState().sessionVersion;
useAuthStore.getState().setTokens(tokens(user(), 'renewal'));
expect(useAuthStore.getState().sessionVersion).toBe(version);
expect(useAuthStore.getState().user?.clinicIds).toEqual(['puer']);
});
it('does not resurrect data after logout and clears patient/assistant context', async () => {
const old = deferred();
getSession.mockReturnValue(old.promise);
useAuthStore.getState().setTokens(tokens(user()));
await tick();
useAssistantStore.getState().ask('old patient');
useAssistantStore.getState().setActiveClinicId('puer');
usePlanSyncStore.getState().setCurrent({ planId: 'p1', patientId: 'old', patientName: 'old' });
useAuthStore.getState().clear();
old.resolve(session('puer'));
await tick();
expect(useAuthStore.getState().user).toBeNull();
expect(useAssistantStore.getState().pending).toBeNull();
expect(useAssistantStore.getState().activeClinicId).toBeNull();
expect(usePlanSyncStore.getState().current).toBeNull();
});
it('compares scope sets independently of ordering', () => {
expect(sessionIdentity({ ...user(), orgScope: ['b', 'a'] } as any)).toBe(
sessionIdentity({ ...user(), orgScope: ['a', 'b'] } as any),
);
});
});
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; import { persist, createJSONStorage } from 'zustand/middleware';
import type { AccessTokenPayload } from '@pac/types'; import type { AccessTokenPayload } from '@pac/types';
import { useAssistantStore } from './assistant-store';
import { usePlanSyncStore } from './plan-sync-store';
/** /**
* 前端会话用户 = JWT 解码(role/permissions/sub/tenantId/dictionary/orgScope,即时) * 前端会话用户 = JWT 解码(role/permissions/sub/tenantId/dictionary/orgScope,即时)
...@@ -49,7 +51,29 @@ export function clinicScopeReady(user: SessionUser | null): boolean { ...@@ -49,7 +51,29 @@ export function clinicScopeReady(user: SessionUser | null): boolean {
return Array.isArray(user?.clinicIds); return Array.isArray(user?.clinicIds);
} }
/** 同一账号也可能切品牌、诊所或角色;令牌续期不改变身份边界。 */
export function sessionIdentity(user: SessionUser | null): string {
return user
? JSON.stringify([
user.hostId,
user.tenantId,
user.sub,
user.role,
[...(user.orgScope ?? [])].sort(),
[...(user.permissions ?? [])].sort(),
])
: '';
}
function resetSessionData() {
useAssistantStore.getState().resetSession();
usePlanSyncStore.setState(usePlanSyncStore.getInitialState());
}
interface AuthState { interface AuthState {
/** 只在重新登录/退出/身份范围改变时递增;供所有页面和请求隔离会话。 */
sessionVersion: number;
sessionError: boolean;
accessToken: string | null; accessToken: string | null;
refreshToken: string | null; refreshToken: string | null;
expiresAt: number | null; expiresAt: number | null;
...@@ -97,6 +121,8 @@ function decodeJwt(token: string): AccessTokenPayload | null { ...@@ -97,6 +121,8 @@ function decodeJwt(token: string): AccessTokenPayload | null {
export const useAuthStore = create<AuthState>()( export const useAuthStore = create<AuthState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
sessionVersion: 0,
sessionError: false,
accessToken: null, accessToken: null,
refreshToken: null, refreshToken: null,
expiresAt: null, expiresAt: null,
...@@ -104,20 +130,22 @@ export const useAuthStore = create<AuthState>()( ...@@ -104,20 +130,22 @@ export const useAuthStore = create<AuthState>()(
setTokens: ({ accessToken, refreshToken, expiresIn }) => { setTokens: ({ accessToken, refreshToken, expiresIn }) => {
const decoded = decodeJwt(accessToken); const decoded = decodeJwt(accessToken);
const prev = get().user; const prev = get().user;
// 防闪空:silent refresh 重签 token 时,同一 sub 沿用上次已展开的 clinicIds/sourceUnits const sameSession = Boolean(
// 作占位,避免 user 被 decodeJwt 重置后诊所筛选短暂清空(loadSession 随后会刷新)。 decoded && prev && sessionIdentity(prev) === sessionIdentity(decoded),
// permissions 同理:decodeJwt 拿到的是**旧 token 的快照**,不留住的话每次 silent refresh );
// 都会让新权限的入口(如分配按钮)闪一下消失 —— 正是 loadSession 刚修好的那个问题。 const user = sameSession
const user = ? {
decoded && prev && prev.sub === decoded.sub ...decoded!,
? { clinicIds: prev!.clinicIds,
...decoded, sourceUnits: prev!.sourceUnits,
clinicIds: prev.clinicIds, actionUrls: prev!.actionUrls,
sourceUnits: prev.sourceUnits, dictionary: prev!.dictionary ?? decoded!.dictionary,
permissions: prev.permissions ?? decoded.permissions, }
} : decoded;
: decoded; if (!sameSession) resetSessionData();
set({ set({
sessionVersion: get().sessionVersion + (sameSession ? 0 : 1),
sessionError: false,
accessToken, accessToken,
refreshToken, refreshToken,
expiresAt: Date.now() + expiresIn * 1000, expiresAt: Date.now() + expiresIn * 1000,
...@@ -127,13 +155,19 @@ export const useAuthStore = create<AuthState>()( ...@@ -127,13 +155,19 @@ export const useAuthStore = create<AuthState>()(
void get().loadSession(); void get().loadSession();
}, },
loadSession: async () => { loadSession: async () => {
if (!get().accessToken) return; const { accessToken, sessionVersion } = get();
if (!accessToken) return;
const current = () =>
get().accessToken === accessToken && get().sessionVersion === sessionVersion;
try { try {
const { getSession } = await import('@/lib/auth-api'); const { getSession } = await import('@/lib/auth-api');
if (!current()) return;
const s = await getSession(); const s = await getSession();
if (!current()) return;
set((st) => set((st) =>
st.user st.user
? { ? {
sessionError: false,
user: { user: {
...st.user, ...st.user,
clinicIds: s.clinicIds, clinicIds: s.clinicIds,
...@@ -152,10 +186,20 @@ export const useAuthStore = create<AuthState>()( ...@@ -152,10 +186,20 @@ export const useAuthStore = create<AuthState>()(
: {}, : {},
); );
} catch { } catch {
// 静默:拿不到就保持 token decode 的基础 user(诊所筛选回退为空) if (current()) set({ sessionError: true });
} }
}, },
clear: () => set({ accessToken: null, refreshToken: null, expiresAt: null, user: null }), clear: () => {
resetSessionData();
set((s) => ({
sessionVersion: s.sessionVersion + 1,
sessionError: false,
accessToken: null,
refreshToken: null,
expiresAt: null,
user: null,
}));
},
isAuthenticated: () => { isAuthenticated: () => {
const { accessToken, expiresAt } = get(); const { accessToken, expiresAt } = get();
return Boolean(accessToken && expiresAt && Date.now() < expiresAt); return Boolean(accessToken && expiresAt && Date.now() < expiresAt);
...@@ -164,7 +208,7 @@ export const useAuthStore = create<AuthState>()( ...@@ -164,7 +208,7 @@ export const useAuthStore = create<AuthState>()(
}), }),
{ {
name: 'pac-auth', name: 'pac-auth',
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => window.localStorage),
// 只持久化原始 token,派生字段(user)反水合时从 token 重新 decode // 只持久化原始 token,派生字段(user)反水合时从 token 重新 decode
partialize: (s) => ({ partialize: (s) => ({
accessToken: s.accessToken, accessToken: s.accessToken,
......
...@@ -4,5 +4,5 @@ import { defineConfig } from 'vitest/config'; ...@@ -4,5 +4,5 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
esbuild: { jsx: 'automatic' }, esbuild: { jsx: 'automatic' },
test: { maxWorkers: 1 }, test: { maxWorkers: 1, setupFiles: ['./vitest.setup.ts'] },
}); });
// Node 26 的原生 localStorage getter 会遮住 jsdom 的 Storage。
// 只给缺少 Storage 的测试环境补浏览器等价内存实现,不影响应用代码。
if (typeof window !== 'undefined' && !window.localStorage) {
const values = new Map<string, string>();
const storage: Storage = {
get length() { return values.size; },
clear: () => values.clear(),
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => { values.delete(key); },
setItem: (key, value) => { values.set(key, String(value)); },
};
Object.defineProperty(window, 'localStorage', { configurable: true, value: storage });
}
...@@ -691,6 +691,9 @@ importers: ...@@ -691,6 +691,9 @@ importers:
eslint-config-next: eslint-config-next:
specifier: ^16.2.4 specifier: ^16.2.4
version: 16.2.4(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) version: 16.2.4(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
jsdom:
specifier: ^26.1.0
version: 26.1.0
tailwindcss: tailwindcss:
specifier: ^4.2.4 specifier: ^4.2.4
version: 4.2.4 version: 4.2.4
...@@ -699,7 +702,7 @@ importers: ...@@ -699,7 +702,7 @@ importers:
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: ^3.2.7 specifier: ^3.2.7
version: 3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2) version: 3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.2)
packages/design-tokens: {} packages/design-tokens: {}
...@@ -859,6 +862,9 @@ packages: ...@@ -859,6 +862,9 @@ packages:
'@apm-js-collab/tracing-hooks@0.13.0': '@apm-js-collab/tracing-hooks@0.13.0':
resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
...@@ -1073,6 +1079,34 @@ packages: ...@@ -1073,6 +1079,34 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.1.0':
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@dabh/diagnostics@2.0.8': '@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
...@@ -4386,6 +4420,10 @@ packages: ...@@ -4386,6 +4420,10 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ai@6.0.184: ai@6.0.184:
resolution: {integrity: sha512-j//zHkKvj5ra27l8izHco8cj1g1Pr7vx1ZK+hrzrkHvndgIRmdfZKOb6+RAPpvbk42qGIsuYvlYbGlVAu3erNQ==} resolution: {integrity: sha512-j//zHkKvj5ra27l8izHco8cj1g1Pr7vx1ZK+hrzrkHvndgIRmdfZKOb6+RAPpvbk42qGIsuYvlYbGlVAu3erNQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -5019,6 +5057,10 @@ packages: ...@@ -5019,6 +5057,10 @@ packages:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
...@@ -5184,6 +5226,10 @@ packages: ...@@ -5184,6 +5226,10 @@ packages:
damerau-levenshtein@1.0.8: damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
...@@ -5219,6 +5265,9 @@ packages: ...@@ -5219,6 +5265,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.3.0: decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
...@@ -6175,6 +6224,10 @@ packages: ...@@ -6175,6 +6224,10 @@ packages:
resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
html-escaper@2.0.2: html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
...@@ -6191,6 +6244,10 @@ packages: ...@@ -6191,6 +6244,10 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
http2-wrapper@2.2.1: http2-wrapper@2.2.1:
resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==}
engines: {node: '>=10.19.0'} engines: {node: '>=10.19.0'}
...@@ -6199,6 +6256,10 @@ packages: ...@@ -6199,6 +6256,10 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
human-signals@2.1.0: human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
...@@ -6380,6 +6441,9 @@ packages: ...@@ -6380,6 +6441,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-promise@4.0.0: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
...@@ -6624,6 +6688,15 @@ packages: ...@@ -6624,6 +6688,15 @@ packages:
resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==}
hasBin: true hasBin: true
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsesc@3.1.0: jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -7324,6 +7397,9 @@ packages: ...@@ -7324,6 +7397,9 @@ packages:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'} engines: {node: '>=8'}
nwsapi@2.2.28:
resolution: {integrity: sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==}
nypm@0.6.6: nypm@0.6.6:
resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -7893,6 +7969,9 @@ packages: ...@@ -7893,6 +7969,9 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
...@@ -7927,6 +8006,10 @@ packages: ...@@ -7927,6 +8006,10 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
...@@ -8258,6 +8341,9 @@ packages: ...@@ -8258,6 +8341,9 @@ packages:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
synckit@0.11.12: synckit@0.11.12:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
...@@ -8346,6 +8432,13 @@ packages: ...@@ -8346,6 +8432,13 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tldts-core@6.1.86:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
tldts@6.1.86:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
tmpl@1.0.5: tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
...@@ -8361,9 +8454,17 @@ packages: ...@@ -8361,9 +8454,17 @@ packages:
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
...@@ -8717,6 +8818,10 @@ packages: ...@@ -8717,6 +8818,10 @@ packages:
jsdom: jsdom:
optional: true optional: true
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
walker@1.0.8: walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
...@@ -8733,6 +8838,10 @@ packages: ...@@ -8733,6 +8838,10 @@ packages:
webidl-conversions@3.0.1: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
webpack-node-externals@3.0.0: webpack-node-externals@3.0.0:
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -8751,6 +8860,19 @@ packages: ...@@ -8751,6 +8860,19 @@ packages:
webpack-cli: webpack-cli:
optional: true optional: true
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@5.0.0: whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
...@@ -8838,6 +8960,13 @@ packages: ...@@ -8838,6 +8960,13 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xmlhttprequest-ssl@2.1.2: xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
...@@ -9014,6 +9143,14 @@ snapshots: ...@@ -9014,6 +9143,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
...@@ -9251,6 +9388,26 @@ snapshots: ...@@ -9251,6 +9388,26 @@ snapshots:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.1.0
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
'@dabh/diagnostics@2.0.8': '@dabh/diagnostics@2.0.8':
dependencies: dependencies:
'@so-ric/colorspace': 1.1.6 '@so-ric/colorspace': 1.1.6
...@@ -12609,6 +12766,8 @@ snapshots: ...@@ -12609,6 +12766,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
agent-base@7.1.4: {}
ai@6.0.184(zod@4.4.3): ai@6.0.184(zod@4.4.3):
dependencies: dependencies:
'@ai-sdk/gateway': 3.0.115(zod@4.4.3) '@ai-sdk/gateway': 3.0.115(zod@4.4.3)
...@@ -13258,6 +13417,11 @@ snapshots: ...@@ -13258,6 +13417,11 @@ snapshots:
crypto-js@4.2.0: {} crypto-js@4.2.0: {}
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
csstype@3.2.3: {} csstype@3.2.3: {}
csv-parse@6.2.1: {} csv-parse@6.2.1: {}
...@@ -13448,6 +13612,11 @@ snapshots: ...@@ -13448,6 +13612,11 @@ snapshots:
damerau-levenshtein@1.0.8: {} damerau-levenshtein@1.0.8: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
...@@ -13478,6 +13647,8 @@ snapshots: ...@@ -13478,6 +13647,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.3.0: decode-named-character-reference@1.3.0:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
...@@ -13804,8 +13975,8 @@ snapshots: ...@@ -13804,8 +13975,8 @@ snapshots:
'@next/eslint-plugin-next': 16.2.4 '@next/eslint-plugin-next': 16.2.4
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0))
...@@ -13827,7 +13998,7 @@ snapshots: ...@@ -13827,7 +13998,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
...@@ -13838,22 +14009,22 @@ snapshots: ...@@ -13838,22 +14009,22 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
...@@ -13864,7 +14035,7 @@ snapshots: ...@@ -13864,7 +14035,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.4 hasown: 2.0.4
is-core-module: 2.16.2 is-core-module: 2.16.2
is-glob: 4.0.3 is-glob: 4.0.3
...@@ -14725,6 +14896,10 @@ snapshots: ...@@ -14725,6 +14896,10 @@ snapshots:
hono@4.12.25: {} hono@4.12.25: {}
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
html-escaper@2.0.2: {} html-escaper@2.0.2: {}
html-url-attributes@3.0.1: {} html-url-attributes@3.0.1: {}
...@@ -14741,6 +14916,13 @@ snapshots: ...@@ -14741,6 +14916,13 @@ snapshots:
statuses: 2.0.2 statuses: 2.0.2
toidentifier: 1.0.1 toidentifier: 1.0.1
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
http2-wrapper@2.2.1: http2-wrapper@2.2.1:
dependencies: dependencies:
quick-lru: 5.1.1 quick-lru: 5.1.1
...@@ -14753,6 +14935,13 @@ snapshots: ...@@ -14753,6 +14935,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
human-signals@2.1.0: {} human-signals@2.1.0: {}
iconv-lite@0.6.3: iconv-lite@0.6.3:
...@@ -14926,6 +15115,8 @@ snapshots: ...@@ -14926,6 +15115,8 @@ snapshots:
is-plain-obj@4.1.0: {} is-plain-obj@4.1.0: {}
is-potential-custom-element-name@1.0.1: {}
is-promise@4.0.0: {} is-promise@4.0.0: {}
is-reference@1.2.1: is-reference@1.2.1:
...@@ -15365,6 +15556,33 @@ snapshots: ...@@ -15365,6 +15556,33 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
jsdom@26.1.0:
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.28
parse5: 7.3.0
rrweb-cssom: 0.8.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 5.1.2
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.21.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
jsesc@3.1.0: {} jsesc@3.1.0: {}
json-bigint@1.0.0: json-bigint@1.0.0:
...@@ -16264,6 +16482,8 @@ snapshots: ...@@ -16264,6 +16482,8 @@ snapshots:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
nwsapi@2.2.28: {}
nypm@0.6.6: nypm@0.6.6:
dependencies: dependencies:
citty: 0.2.2 citty: 0.2.2
...@@ -16927,6 +17147,8 @@ snapshots: ...@@ -16927,6 +17147,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
rrweb-cssom@0.8.0: {}
run-parallel@1.2.0: run-parallel@1.2.0:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
...@@ -16966,6 +17188,10 @@ snapshots: ...@@ -16966,6 +17188,10 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
scheduler@0.27.0: {} scheduler@0.27.0: {}
schema-utils@3.3.0: schema-utils@3.3.0:
...@@ -17391,6 +17617,8 @@ snapshots: ...@@ -17391,6 +17617,8 @@ snapshots:
symbol-observable@4.0.0: {} symbol-observable@4.0.0: {}
symbol-tree@3.2.4: {}
synckit@0.11.12: synckit@0.11.12:
dependencies: dependencies:
'@pkgr/core': 0.2.9 '@pkgr/core': 0.2.9
...@@ -17484,6 +17712,12 @@ snapshots: ...@@ -17484,6 +17712,12 @@ snapshots:
tinyspy@4.0.4: {} tinyspy@4.0.4: {}
tldts-core@6.1.86: {}
tldts@6.1.86:
dependencies:
tldts-core: 6.1.86
tmpl@1.0.5: {} tmpl@1.0.5: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
...@@ -17498,8 +17732,16 @@ snapshots: ...@@ -17498,8 +17732,16 @@ snapshots:
'@tokenizer/token': 0.3.0 '@tokenizer/token': 0.3.0
ieee754: 1.2.1 ieee754: 1.2.1
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
tr46@0.0.3: {} tr46@0.0.3: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
triple-beam@1.4.1: {} triple-beam@1.4.1: {}
...@@ -17857,7 +18099,7 @@ snapshots: ...@@ -17857,7 +18099,7 @@ snapshots:
lightningcss: 1.32.0 lightningcss: 1.32.0
terser: 5.46.2 terser: 5.46.2
vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2): vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.19.17)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.46.2):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/expect': 3.2.7 '@vitest/expect': 3.2.7
...@@ -17885,6 +18127,7 @@ snapshots: ...@@ -17885,6 +18127,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/debug': 4.1.13 '@types/debug': 4.1.13
'@types/node': 22.19.17 '@types/node': 22.19.17
jsdom: 26.1.0
transitivePeerDependencies: transitivePeerDependencies:
- jiti - jiti
- less - less
...@@ -17899,6 +18142,10 @@ snapshots: ...@@ -17899,6 +18142,10 @@ snapshots:
- tsx - tsx
- yaml - yaml
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
walker@1.0.8: walker@1.0.8:
dependencies: dependencies:
makeerror: 1.0.12 makeerror: 1.0.12
...@@ -17916,6 +18163,8 @@ snapshots: ...@@ -17916,6 +18163,8 @@ snapshots:
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
webpack-node-externals@3.0.0: {} webpack-node-externals@3.0.0: {}
webpack-sources@3.4.1: {} webpack-sources@3.4.1: {}
...@@ -17984,6 +18233,17 @@ snapshots: ...@@ -17984,6 +18233,17 @@ snapshots:
- esbuild - esbuild
- uglify-js - uglify-js
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
whatwg-url@5.0.0: whatwg-url@5.0.0:
dependencies: dependencies:
tr46: 0.0.3 tr46: 0.0.3
...@@ -18092,6 +18352,10 @@ snapshots: ...@@ -18092,6 +18352,10 @@ snapshots:
ws@8.21.0: {} ws@8.21.0: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
xmlhttprequest-ssl@2.1.2: {} xmlhttprequest-ssl@2.1.2: {}
xtend@4.0.2: {} xtend@4.0.2: {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment