diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f0aaa5..32901aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,20 +18,18 @@ jobs: web: runs-on: ubuntu-latest - defaults: - run: - working-directory: apps/web steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - cache-dependency-path: apps/web/package-lock.json - - run: npm ci --workspaces=false - - run: npm run lint - - run: npm run test - - run: npm run build + cache-dependency-path: package-lock.json + - run: npm ci + - run: npm --prefix apps/web install --no-save --package-lock=false --ignore-scripts @tailwindcss/oxide-linux-x64-gnu@4.2.2 lightningcss-linux-x64-gnu@1.32.0 react@19.2.4 react-dom@19.2.4 + - run: npm --prefix apps/web run lint + - run: npm --prefix apps/web run test + - run: npm --prefix apps/web run build api-go: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 4b9f041..0c395a3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![Go](https://img.shields.io/badge/Go-1.22+-00ADD8?logo=go)](https://go.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript)](https://www.typescriptlang.org) -**Live Demo** · [diffaudit.vectorcontrol.tech](https://diffaudit.vectorcontrol.tech) +**Demo** · Run locally or deploy your own instance [English](#english) · [简体中文](#简体中文) @@ -151,7 +151,7 @@ DiffAudit Platform 围绕三个问题提供答案: ### 快速体验 -在线 Demo:[diffaudit.vectorcontrol.tech](https://diffaudit.vectorcontrol.tech) +在线 Demo:请部署自己的实例,或按下方命令在本地启动。 本地启动: diff --git a/apps/web/src/app/(workspace)/workspace/audits/TaskListClient.tsx b/apps/web/src/app/(workspace)/workspace/audits/TaskListClient.tsx index 27bf458..db6db21 100644 --- a/apps/web/src/app/(workspace)/workspace/audits/TaskListClient.tsx +++ b/apps/web/src/app/(workspace)/workspace/audits/TaskListClient.tsx @@ -41,12 +41,13 @@ function trackTone(job: JobRecord): Tone { return "muted"; } -function trackIcon(tone: Tone) { - if (tone === "green") return Shield; - if (tone === "purple") return Eye; - if (tone === "blue") return Activity; - if (tone === "orange") return Search; - return Activity; +function TrackIcon({ tone, size }: { tone: Tone; size: number }) { + const props = { size, strokeWidth: 1.7, "aria-hidden": true as const }; + if (tone === "green") return ; + if (tone === "purple") return ; + if (tone === "blue") return ; + if (tone === "orange") return ; + return ; } function statusToneClass(status: string): string { @@ -75,7 +76,6 @@ function formatRemaining(progressPct: number, createdAt: string, locale: Locale, export function RunningCard({ job, locale }: { job: JobRecord; locale: Locale }) { const copy = WORKSPACE_COPY[locale].audits; const tone = trackTone(job); - const Icon = trackIcon(tone); const pct = typeof job.progress_pct === "number" ? Math.round(job.progress_pct) : 0; const [now, setNow] = useState(() => Date.now()); @@ -93,7 +93,7 @@ export function RunningCard({ job, locale }: { job: JobRecord; locale: Locale }) className="audits-running-card" > -
@@ -201,14 +201,13 @@ export function HistoryTable({ jobs, locale, loading, loadError, onRefresh }: Hi {jobs.map((job) => { const tone = trackTone(job); - const Icon = trackIcon(tone); const reportHref = buildCompletedJobReportHref(job); return (
-
{job.job_id} diff --git a/apps/web/src/app/(workspace)/workspace/audits/page.tsx b/apps/web/src/app/(workspace)/workspace/audits/page.tsx index f4a75d9..5b46698 100644 --- a/apps/web/src/app/(workspace)/workspace/audits/page.tsx +++ b/apps/web/src/app/(workspace)/workspace/audits/page.tsx @@ -8,9 +8,7 @@ export const dynamic = "force-dynamic"; import { resolveLocaleFromHeaderStore } from "@/lib/locale"; import { WORKSPACE_COPY } from "@/lib/workspace-copy"; import { WorkspacePageFrame } from "@/components/workspace-frame"; -import { sanitizeAuditJobPayload } from "@/lib/audit-job-payload"; -import { isDemoModeEnabledServer } from "@/lib/demo-mode"; -import { listDemoJobs } from "@/lib/demo-jobs-store"; +import { getWorkspaceAuditJobsData } from "@/lib/workspace-source"; import { AuditsPageClient } from "./AuditsPageClient"; type WorkspaceAuditsPageOptions = { @@ -20,9 +18,7 @@ type WorkspaceAuditsPageOptions = { async function renderWorkspaceAuditsPage({ locale }: WorkspaceAuditsPageOptions = {}) { const resolvedLocale = locale ?? resolveLocaleFromHeaderStore(await headers()); const copy = WORKSPACE_COPY[resolvedLocale].audits; - const initialJobs = await isDemoModeEnabledServer() - ? sanitizeAuditJobPayload(listDemoJobs()) - : []; + const initialJobs = await getWorkspaceAuditJobsData(); return ( { const { default: WorkspaceHomePage } = await import("./start/page"); const markup = await renderMarkup(await WorkspaceHomePage()); - expect(markup).toContain("建议操作"); - expect(markup).toContain("最近结果"); + expect(markup).toContain("工作台总览"); + expect(markup).toContain("AUC 风险分布"); + expect(markup).toContain("近期任务"); expect(markup).toContain("PIA"); expect(markup).toContain("stable-diffusion-v1-4"); - expect(markup).toContain("可审计模型"); - expect(markup).toContain("已防御结果"); + expect(markup).toContain("可审计合同"); + expect(markup).toContain("已评估防御"); }); it("renders en-US copy with forced demo data", async () => { @@ -44,9 +45,10 @@ describe("WorkspaceHomePage", () => { const { default: WorkspaceHomePage } = await import("./start/page"); const markup = await renderMarkup(await WorkspaceHomePage()); - expect(markup).toContain("Suggested actions"); - expect(markup).toContain("Recent results"); + expect(markup).toContain("Workspace Overview"); + expect(markup).toContain("AUC Risk Distribution"); + expect(markup).toContain("Recent tasks"); expect(markup).toContain("stable-diffusion-v1-4"); - expect(markup).toContain("Auditable models"); + expect(markup).toContain("Auditable contracts"); }); }); diff --git a/apps/web/src/app/(workspace)/workspace/reports/page.test.tsx b/apps/web/src/app/(workspace)/workspace/reports/page.test.tsx index d5823c3..1900eb3 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/page.test.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/page.test.tsx @@ -25,11 +25,11 @@ describe("WorkspaceReportsPage", () => { const { default: WorkspaceReportsPage } = await import("./page"); const markup = await renderMarkup(await WorkspaceReportsPage()); - expect(markup).toContain("报告生成"); - expect(markup).toContain("按审计模式生成"); - expect(markup).toContain("已生成报告"); - expect(markup).toContain("综合分析"); - expect(markup).toContain("导出选项"); + expect(markup).toContain("审计结果和覆盖缺口"); + expect(markup).toContain("任务报告"); + expect(markup).toContain("任务报告表"); + expect(markup).toContain("photo-real-xl"); + expect(markup).toContain("查看审计报告"); }); it("renders en-US copy with forced demo data", async () => { @@ -37,11 +37,10 @@ describe("WorkspaceReportsPage", () => { const { default: WorkspaceReportsPage } = await import("./page"); const markup = await renderMarkup(await WorkspaceReportsPage()); - expect(markup).toContain("Reports"); - expect(markup).toContain("Report Generation"); - expect(markup).toContain("Generate by Audit Mode"); - expect(markup).toContain("Generated Reports"); - expect(markup).toContain("Comprehensive Analysis"); - expect(markup).toContain("Export Options"); + expect(markup).toContain("Audit results and coverage gaps"); + expect(markup).toContain("Task reports"); + expect(markup).toContain("Task reports table"); + expect(markup).toContain("photo-real-xl"); + expect(markup).toContain("View Report"); }); }); diff --git a/apps/web/src/app/(workspace)/workspace/reports/page.tsx b/apps/web/src/app/(workspace)/workspace/reports/page.tsx index 8d1b031..77d86d2 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/page.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/page.tsx @@ -3,9 +3,7 @@ import { headers } from "next/headers"; import { resolveLocaleFromHeaderStore } from "@/lib/locale"; import { WORKSPACE_COPY } from "@/lib/workspace-copy"; import { WorkspacePageFrame } from "@/components/workspace-frame"; -import { sanitizeAuditJobPayload } from "@/lib/audit-job-payload"; -import { isDemoModeEnabledServer } from "@/lib/demo-mode"; -import { listDemoJobs } from "@/lib/demo-jobs-store"; +import { getWorkspaceAuditJobsData } from "@/lib/workspace-source"; import { ReportsPageClient } from "./ReportsPageClient"; export const dynamic = "force-dynamic"; @@ -17,9 +15,7 @@ type WorkspaceReportsPageOptions = { async function renderWorkspaceReportsPage({ locale }: WorkspaceReportsPageOptions = {}) { const resolvedLocale = locale ?? resolveLocaleFromHeaderStore(await headers()); const copy = WORKSPACE_COPY[resolvedLocale].reports; - const initialJobs = await isDemoModeEnabledServer() - ? sanitizeAuditJobPayload(listDemoJobs()) - : []; + const initialJobs = await getWorkspaceAuditJobsData(); return ( diff --git a/apps/web/src/app/api/auth/me/route.test.ts b/apps/web/src/app/api/auth/me/route.test.ts new file mode 100644 index 0000000..69e525f --- /dev/null +++ b/apps/web/src/app/api/auth/me/route.test.ts @@ -0,0 +1,62 @@ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const getCurrentUserProfile = vi.fn(); +const isDemoModeEnabledServer = vi.fn(); + +vi.mock("@/lib/auth", () => ({ + getCurrentUserProfile, + SESSION_COOKIE_NAME: "diffaudit_session", +})); + +vi.mock("@/lib/demo-mode", () => ({ + isDemoModeEnabledServer, +})); + +describe("auth me route", () => { + beforeEach(() => { + vi.resetModules(); + getCurrentUserProfile.mockReset(); + isDemoModeEnabledServer.mockReset(); + }); + + it("returns anonymous profile without a 401 response when demo mode is enabled", async () => { + isDemoModeEnabledServer.mockResolvedValue(true); + const route = await import("./route"); + + const response = await route.GET(new NextRequest("http://localhost/api/auth/me")); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload).toEqual({ user: null }); + expect(getCurrentUserProfile).not.toHaveBeenCalled(); + }); + + it("keeps the unauthenticated 401 response outside demo mode", async () => { + isDemoModeEnabledServer.mockResolvedValue(false); + const route = await import("./route"); + + const response = await route.GET(new NextRequest("http://localhost/api/auth/me")); + const payload = await response.json(); + + expect(response.status).toBe(401); + expect(payload).toEqual({ user: null }); + }); + + it("returns the current user when a valid session exists", async () => { + const user = { id: "user-1", username: "demo-reviewer" }; + getCurrentUserProfile.mockReturnValue(user); + const route = await import("./route"); + + const response = await route.GET( + new NextRequest("http://localhost/api/auth/me", { + headers: { cookie: "diffaudit_session=session-token" }, + }), + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload).toEqual({ user }); + expect(getCurrentUserProfile).toHaveBeenCalledWith("session-token"); + }); +}); diff --git a/apps/web/src/app/api/auth/me/route.ts b/apps/web/src/app/api/auth/me/route.ts index 30e917a..94ff7fa 100644 --- a/apps/web/src/app/api/auth/me/route.ts +++ b/apps/web/src/app/api/auth/me/route.ts @@ -1,16 +1,25 @@ import { type NextRequest } from "next/server"; import { getCurrentUserProfile, SESSION_COOKIE_NAME } from "@/lib/auth"; +import { isDemoModeEnabledServer } from "@/lib/demo-mode"; + +async function anonymousResponse(request: NextRequest) { + if (await isDemoModeEnabledServer(request)) { + return Response.json({ user: null }); + } + + return Response.json({ user: null }, { status: 401 }); +} export async function GET(request: NextRequest) { const token = request.cookies.get(SESSION_COOKIE_NAME)?.value; if (!token) { - return Response.json({ user: null }, { status: 401 }); + return anonymousResponse(request); } const user = getCurrentUserProfile(token); if (!user) { - return Response.json({ user: null }, { status: 401 }); + return anonymousResponse(request); } return Response.json({ user }); diff --git a/apps/web/src/app/health/route.test.ts b/apps/web/src/app/health/route.test.ts new file mode 100644 index 0000000..193427a --- /dev/null +++ b/apps/web/src/app/health/route.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const isDemoModeEnabledServer = vi.fn(); +const proxyToBackend = vi.fn(); + +vi.mock("@/lib/demo-mode", () => ({ + isDemoModeEnabledServer, +})); + +vi.mock("@/lib/api-proxy", () => ({ + proxyToBackend, +})); + +describe("health route", () => { + beforeEach(() => { + vi.resetModules(); + isDemoModeEnabledServer.mockReset(); + proxyToBackend.mockReset(); + proxyToBackend.mockResolvedValue(Response.json({ upstream: true })); + }); + + it("returns demo health without proxying when demo mode is enabled", async () => { + isDemoModeEnabledServer.mockResolvedValue(true); + const route = await import("./route"); + + const response = await route.GET(new Request("http://localhost/health")); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload).toMatchObject({ + demo_mode: true, + snapshot_available: true, + build: { revision: "demo-snapshot" }, + }); + expect(proxyToBackend).not.toHaveBeenCalled(); + }); + + it("proxies backend health outside demo mode", async () => { + isDemoModeEnabledServer.mockResolvedValue(false); + const route = await import("./route"); + + await route.GET(new Request("http://localhost/health")); + + expect(proxyToBackend).toHaveBeenCalledWith("/health"); + }); +}); diff --git a/apps/web/src/app/health/route.ts b/apps/web/src/app/health/route.ts index c5f5598..ebde043 100644 --- a/apps/web/src/app/health/route.ts +++ b/apps/web/src/app/health/route.ts @@ -1,5 +1,16 @@ import { proxyToBackend } from "@/lib/api-proxy"; +import { isDemoModeEnabledServer } from "@/lib/demo-mode"; + +export async function GET(request: Request) { + if (await isDemoModeEnabledServer(request)) { + return Response.json({ + demo_mode: true, + snapshot_available: true, + build: { + revision: "demo-snapshot", + }, + }); + } -export async function GET() { return proxyToBackend("/health"); } diff --git a/apps/web/src/components/chart-risk-donut.tsx b/apps/web/src/components/chart-risk-donut.tsx index b8aa790..0e52867 100644 --- a/apps/web/src/components/chart-risk-donut.tsx +++ b/apps/web/src/components/chart-risk-donut.tsx @@ -27,17 +27,20 @@ export function ChartRiskDonut({ data, totalLabel = "总结果", height = 160 }: const router = useRouter(); const total = data.reduce((acc, item) => acc + item.count, 0); const chartSize = Math.max(106, Math.min(124, height - 46)); - let cursor = 0; + const segments = data.map((item, index) => { + const start = data + .slice(0, index) + .reduce((acc, previous) => acc + (total > 0 ? (previous.count / total) * 360 : 0), 0); + const sweep = total > 0 ? (item.count / total) * 360 : 0; + return { item, start, sweep }; + }); return (
- {data.map((item) => { - const start = cursor; - const sweep = total > 0 ? (item.count / total) * 360 : 0; - cursor += sweep; + {segments.map(({ item, start, sweep }) => { return ( { - setActiveIndex(0); - }, [query]); - /* ---- Scroll active item into view ---- */ useEffect(() => { if (!listRef.current) return; @@ -393,7 +388,10 @@ export function CommandPalette({ locale }: { locale: Locale }) { aria-controls="command-listbox" aria-activedescendant={flatItems[activeIndex] ? `cmd-${flatItems[activeIndex].id}` : undefined} value={query} - onChange={(e) => setQuery(e.target.value)} + onChange={(e) => { + setQuery(e.target.value); + setActiveIndex(0); + }} onKeyDown={onInputKeyDown} /> ESC diff --git a/apps/web/src/components/navigation-progress.tsx b/apps/web/src/components/navigation-progress.tsx index 20e3293..f4561b4 100644 --- a/apps/web/src/components/navigation-progress.tsx +++ b/apps/web/src/components/navigation-progress.tsx @@ -9,14 +9,15 @@ import { usePathname } from "next/navigation"; */ export function NavigationProgress() { const pathname = usePathname(); - const [progress, setProgress] = useState(0); - const [visible, setVisible] = useState(false); - useEffect(() => { - // Start progress on route change - setVisible(true); - setProgress(30); + return ; +} +function NavigationProgressBar() { + const [progress, setProgress] = useState(30); + const [visible, setVisible] = useState(true); + + useEffect(() => { const t1 = setTimeout(() => setProgress(60), 100); const t2 = setTimeout(() => setProgress(80), 300); const t3 = setTimeout(() => setProgress(100), 500); @@ -31,7 +32,7 @@ export function NavigationProgress() { clearTimeout(t3); clearTimeout(t4); }; - }, [pathname]); + }, []); if (!visible) return null; diff --git a/apps/web/src/components/workspace-global-search.tsx b/apps/web/src/components/workspace-global-search.tsx index 00ac6ac..904c6fe 100644 --- a/apps/web/src/components/workspace-global-search.tsx +++ b/apps/web/src/components/workspace-global-search.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { Clock, ArrowRight } from "lucide-react"; import { type Locale } from "@/components/language-picker"; import { getNavItems } from "@/lib/navigation"; @@ -87,11 +86,6 @@ export function WorkspaceGlobalSearch({ locale }: { locale: Locale }) { .slice(0, 6); }, [items, query]); - // Reset active index when matches change - useEffect(() => { - setActiveIndex(0); - }, [matches]); - useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { @@ -151,6 +145,7 @@ export function WorkspaceGlobalSearch({ locale }: { locale: Locale }) { value={query} onChange={(event) => { setQuery(event.target.value); + setActiveIndex(0); setOpen(true); }} onFocus={() => setOpen(true)} diff --git a/apps/web/src/components/workspace-sidebar.tsx b/apps/web/src/components/workspace-sidebar.tsx index 781e30e..513e141 100644 --- a/apps/web/src/components/workspace-sidebar.tsx +++ b/apps/web/src/components/workspace-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useRef } from "react"; +import { useEffect, useCallback, useRef, useSyncExternalStore } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { ChevronLeft, Moon, Sun } from "lucide-react"; @@ -13,6 +13,7 @@ import { findActiveNavItem } from "@/lib/platform-shell"; import { WORKSPACE_COPY } from "@/lib/workspace-copy"; const STORAGE_KEY = "diffaudit-sidebar-collapsed"; +const STORAGE_EVENT = "diffaudit:sidebar-collapsed"; // Account-related entries get visually grouped at the bottom of the sidebar. const ACCOUNT_GROUP_KEYS: ReadonlySet = new Set(["apiKeys", "account", "settings"]); @@ -26,6 +27,16 @@ function getCollapsedFromStorage(): boolean { } } +function subscribeCollapsedStorage(onStoreChange: () => void) { + if (typeof window === "undefined") return () => {}; + window.addEventListener("storage", onStoreChange); + window.addEventListener(STORAGE_EVENT, onStoreChange); + return () => { + window.removeEventListener("storage", onStoreChange); + window.removeEventListener(STORAGE_EVENT, onStoreChange); + }; +} + export function WorkspaceSidebar({ locale = "en-US" }: { locale?: Locale }) { const pathname = usePathname(); const { resolvedTheme, toggle } = useTheme(); @@ -36,22 +47,16 @@ export function WorkspaceSidebar({ locale = "en-US" }: { locale?: Locale }) { const themeLabel = isDark ? WORKSPACE_COPY[locale].userMenu.themeDark : WORKSPACE_COPY[locale].userMenu.themeLight; - const [collapsed, setCollapsed] = useState(false); - - useEffect(() => { - setCollapsed(getCollapsedFromStorage()); - }, []); + const collapsed = useSyncExternalStore(subscribeCollapsedStorage, getCollapsedFromStorage, () => false); const toggleCollapse = useCallback(() => { - setCollapsed((prev) => { - const next = !prev; - try { - localStorage.setItem(STORAGE_KEY, String(next)); - } catch { - // localStorage may be unavailable - } - return next; - }); + const next = !getCollapsedFromStorage(); + try { + localStorage.setItem(STORAGE_KEY, String(next)); + } catch { + // localStorage may be unavailable + } + window.dispatchEvent(new Event(STORAGE_EVENT)); }, []); // Expose toggle for keyboard shortcut diff --git a/apps/web/src/lib/workspace-source.ts b/apps/web/src/lib/workspace-source.ts index d2b0cba..ff70e92 100644 --- a/apps/web/src/lib/workspace-source.ts +++ b/apps/web/src/lib/workspace-source.ts @@ -1,3 +1,6 @@ +import { + sanitizeAuditJobPayload, +} from "@/lib/audit-job-payload"; import { fetchAttackDefenseTable, type AttackDefenseRowViewModel, @@ -9,6 +12,7 @@ import { type CatalogEntryViewModel, type CatalogTrack, } from "@/lib/catalog"; +import { listDemoJobs } from "@/lib/demo-jobs-store"; import { isDemoModeEnabledServer, isDemoModeForcedServer } from "@/lib/demo-mode"; export type { @@ -46,3 +50,9 @@ export async function getWorkspaceCatalogData(): Promise { return fetchAttackDefenseTable(); } + +export async function getWorkspaceAuditJobsData(): Promise> { + return (await isDemoModeEnabledServer()) + ? sanitizeAuditJobPayload(listDemoJobs()) + : []; +} diff --git a/package-lock.json b/package-lock.json index 99779eb..3c32fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -527,6 +527,111 @@ "fast-glob": "3.3.1" } }, + "apps/web/node_modules/@next/swc-darwin-arm64": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-darwin-x64": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "apps/web/node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "apps/web/node_modules/@next/swc-win32-x64-msvc": { "version": "16.2.4", "cpu": [ @@ -766,6 +871,26 @@ "url": "https://opencollective.com/immer" } }, + "apps/web/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz", + "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "apps/web/node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.0-rc.13", "cpu": [ @@ -4106,6 +4231,26 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, + "apps/web/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ], + "engines": { + "node": ">= 12.0.0" + } + }, "apps/web/node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", "cpu": [ @@ -6553,111 +6698,6 @@ "node_modules/web": { "resolved": "apps/web", "link": true - }, - "apps/web/node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "apps/web/node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } }