From 72f9a8d2c25cd53527414460ba243eb5e17807f3 Mon Sep 17 00:00:00 2001 From: Vladyslava <71496479+Vladyslava95@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:30:20 +0200 Subject: [PATCH 01/26] Update auth.test.ts --- tests/api/auth.test.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/api/auth.test.ts b/tests/api/auth.test.ts index fb762204..28a3db6b 100644 --- a/tests/api/auth.test.ts +++ b/tests/api/auth.test.ts @@ -1,13 +1,25 @@ -import { test, expect } from '@playwright/test'; +import { test } from './fixtures/base'; +import { expect } from '@playwright/test'; +import { randomUUID } from 'node:crypto'; +// TO DO: investigate how run test with auth and without it -test.fixme('should return success response if the Autorization header is right', async () => { - // const response = await fetchListOfReports(env.API_TOKEN as string); - // expect(response.status).toBe(200); - // expect(await response.text()).not.toBe('Unauthorized'); -}); +// test ('should return success response if the Autorization header is right', async ({request}) => { +// const token = process.env.API_TOKEN; +// const response = await request.get('/api/result/list', { +// headers: { +// Authorization: `${token}`, +// }, +// }); +// expect(response.status()).toBe(200); +// expect(await response.text()).not.toBe('Unauthorized'); +// }); -test.fixme('should return unauthorised if the Autorization header is wrong', async () => { - // const response = await fetchListOfReports(randomUUID()); - // expect(response.status).toBe(401); - // expect(await response.text()).toBe('Unauthorized'); -}); +// test ('should return unauthorised if the Autorization header is wrong', async ({request}) => { +// const response = await request.get('/api/report/list', { +// headers: { +// Authorization: `Bearer ${randomUUID()}`, +// }, +// }); +// expect(response.status()).toBe(401); +// expect(await response.text()).toBe('Unauthorized'); +// }); From ad82f7c6ebf7f5ea11897a0f3a1221013def9e41 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:59:33 +0200 Subject: [PATCH 02/26] fix: info request is triggered twice --- app/components/aside.tsx | 5 ++--- app/components/page-layout.tsx | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/components/aside.tsx b/app/components/aside.tsx index d2a7309f..56bee02a 100644 --- a/app/components/aside.tsx +++ b/app/components/aside.tsx @@ -27,13 +27,12 @@ export const Aside: React.FC = () => { const pathname = usePathname(); const session = useSession(); const { authRequired } = useAuthConfig(); + const isAuthenticated = authRequired === false || session.status === 'authenticated'; const { data: serverInfo } = useQuery('/api/info', { - dependencies: [], + enabled: isAuthenticated, }); - const isAuthenticated = authRequired === false || session.status === 'authenticated'; - return ( diff --git a/app/components/page-layout.tsx b/app/components/page-layout.tsx index 6b621161..4978e7db 100644 --- a/app/components/page-layout.tsx +++ b/app/components/page-layout.tsx @@ -14,12 +14,20 @@ interface PageLayoutProps { render: (props: { info: ServerDataInfo; onUpdate: () => void }) => React.ReactNode; } -export default function PageLayout({ render }: PageLayoutProps) { +export default function PageLayout({ render }: Readonly) { const { data: session, status } = useSession(); const authIsLoading = status === 'loading'; const { authRequired } = useAuthConfig(); + const isAuthenticated = authRequired === false || status === 'authenticated'; - const { data: info, error, refetch, isLoading: isInfoLoading } = useQuery('/api/info'); + const { + data: info, + error, + refetch, + isLoading: isInfoLoading, + } = useQuery('/api/info', { + enabled: isAuthenticated, + }); const [refreshId, setRefreshId] = useState(uuidv4()); useEffect(() => { @@ -34,17 +42,12 @@ export default function PageLayout({ render }: PageLayoutProps) { }, [authIsLoading, session, authRequired]); useLayoutEffect(() => { - // Skip session check if auth is not required - if (authRequired === false) { - refetch(); - + // skip refetch is not authorized + if (authRequired && (authIsLoading || !session)) { return; } - if (authIsLoading || !session) { - return; - } - refetch(); + refetch({ cancelRefetch: false }); }, [refreshId, session, authRequired]); if (authIsLoading || isInfoLoading) { From bbdc48948444a59f063ba509fd3a94366e898042 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:03:20 +0200 Subject: [PATCH 03/26] fix: icon colors --- app/api/report/trend/route.ts | 4 +--- app/components/header-links.tsx | 11 ++++++++++- app/components/icons.tsx | 23 ++++++++++++++++++++--- app/components/trend-chart.tsx | 9 +++++++-- app/styles/globals.css | 4 ++-- readme.md | 2 +- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/api/report/trend/route.ts b/app/api/report/trend/route.ts index 1bda6a47..ea367aa5 100644 --- a/app/api/report/trend/route.ts +++ b/app/api/report/trend/route.ts @@ -7,9 +7,7 @@ export const dynamic = 'force-dynamic'; // defaults to auto export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const project = searchParams.get('project') ?? ''; - const { reports } = await service.getReports({ project }); - - const latestReports = reports.slice(0, 20); + const { reports: latestReports } = await service.getReports({ project, pagination: { offset: 0, limit: 20 } }); return Response.json(latestReports); } diff --git a/app/components/header-links.tsx b/app/components/header-links.tsx index 382a1cd0..3386e636 100644 --- a/app/components/header-links.tsx +++ b/app/components/header-links.tsx @@ -1,6 +1,14 @@ import { Link } from '@heroui/link'; -import { GithubIcon, DiscordIcon, TelegramIcon, LinkIcon, BitbucketIcon, CyborgTestIcon } from '@/app/components/icons'; +import { + GithubIcon, + DiscordIcon, + TelegramIcon, + LinkIcon, + BitbucketIcon, + CyborgTestIcon, + SlackIcon, +} from '@/app/components/icons'; import { SiteWhiteLabelConfig } from '@/app/types'; interface HeaderLinksProps { @@ -17,6 +25,7 @@ export const HeaderLinks: React.FC = ({ config, withTitle = fa { name: 'github', Icon: GithubIcon, title: 'GitHub' }, { name: 'cyborgTest', Icon: CyborgTestIcon, title: 'Cyborg Test' }, { name: 'bitbucket', Icon: BitbucketIcon, title: 'Bitbucket' }, + { name: 'slack', Icon: SlackIcon, title: 'Slack' }, ]; const socialLinks = Object.entries(links).map(([name, href]) => { diff --git a/app/components/icons.tsx b/app/components/icons.tsx index fe8af021..825768f5 100644 --- a/app/components/icons.tsx +++ b/app/components/icons.tsx @@ -26,6 +26,23 @@ export const GithubIcon: FC = ({ size = 40, width, height, ...prop ); }; +export const SlackIcon: FC = ({ size = 40, width, height, ...props }) => ( + + + + + + +); + export const BitbucketIcon: FC = ({ size = 40, width, height, ...props }) => { return ( = ({ size = 40, width, height, ...prop export const TrendIcon: FC = ({ size = 40, width, height, ...props }) => { return ( = ({ size = 24, width, height, ...pr return ( ) { flaky: getPercentage(r.stats.flaky, r.stats.total), flakyCount: r.stats.flaky, total: r.stats.total, - reportUrl: r.reportUrl, + reportUrl: `/report/${r.reportID}`, })); return ( {reportHistory.length <= 1 ? ( - Not enough data for trend chart +
+
+ +
+
) : ( Date: Sat, 8 Nov 2025 17:09:45 +0200 Subject: [PATCH 04/26] chore: use listV2 endpoint for s3 --- app/lib/service/index.ts | 15 +-------------- app/lib/storage/fs.ts | 3 ++- app/lib/storage/s3.ts | 24 +++++------------------- app/lib/time.ts | 7 +++++++ 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/app/lib/service/index.ts b/app/lib/service/index.ts index 8a922a3d..17cb1b2a 100644 --- a/app/lib/service/index.ts +++ b/app/lib/service/index.ts @@ -24,6 +24,7 @@ import { defaultConfig } from '@/app/lib/config'; import { env } from '@/app/config/env'; import { type S3 } from '@/app/lib/storage/s3'; import { isValidPlaywrightVersion } from '@/app/lib/pw'; +import { getTimestamp } from '@/app/lib/time'; class Service { private static instance: Service; @@ -79,13 +80,6 @@ class Service { }); } - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - reports.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); const currentReports = handlePagination(reports, input?.pagination); @@ -215,13 +209,6 @@ class Service { return await storage.readResults(input); } - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - cached.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); let filtered = input?.project diff --git a/app/lib/storage/fs.ts b/app/lib/storage/fs.ts index 051f9c13..4ea1c10f 100644 --- a/app/lib/storage/fs.ts +++ b/app/lib/storage/fs.ts @@ -77,8 +77,9 @@ export async function readFile(targetPath: string, contentType: string | null) { async function getResultsCount() { const files = await fs.readdir(RESULTS_FOLDER); + const zipFilesCount = files.filter((file) => file.endsWith('.zip')); - return Math.round(files.length / 2); + return zipFilesCount.length; } export async function readResults(input?: ReadResultsInput) { diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index e69c76f3..858cf02b 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -1,5 +1,5 @@ -import { randomUUID, type UUID } from 'crypto'; -import fs from 'fs/promises'; +import { randomUUID, type UUID } from 'node:crypto'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { PassThrough, Readable } from 'node:stream'; @@ -42,6 +42,7 @@ import { withError } from '@/app/lib/withError'; import { env } from '@/app/config/env'; import { SiteWhiteLabelConfig } from '@/app/types'; import { defaultConfig, isConfigValid } from '@/app/lib/config'; +import { getTimestamp } from '@/app/lib/time'; const createClient = () => { const endPoint = env.S3_ENDPOINT; @@ -183,7 +184,7 @@ export class S3 implements Storage { let resultCount = 0; let indexCount = 0; let totalSize = 0; - const stream = this.client.listObjects(this.bucket, folderPath, true); + const stream = this.client.listObjectsV2(this.bucket, folderPath, true); return new Promise((resolve, reject) => { stream.on('data', (obj) => { @@ -287,14 +288,6 @@ export class S3 implements Storage { total: 0, }; } - - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - jsonFiles.sort((a, b) => getTimestamp(b.lastModified) - getTimestamp(a.lastModified)); // check if we can apply pagination early @@ -423,13 +416,6 @@ export class S3 implements Storage { }); reportsStream.on('end', async () => { - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - reports.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); const currentReports = handlePagination(reports, input?.pagination); @@ -691,7 +677,7 @@ export class S3 implements Storage { console.error(`[s3] failed to create temporary folder: ${mkdirTempError.message}`); } - const resultsStream = this.client.listObjects(this.bucket, RESULTS_BUCKET, true); + const resultsStream = this.client.listObjectsV2(this.bucket, RESULTS_BUCKET, true); console.log(`[s3] start processing...`); for await (const result of resultsStream) { diff --git a/app/lib/time.ts b/app/lib/time.ts index 175e9be8..fb5c5c68 100644 --- a/app/lib/time.ts +++ b/app/lib/time.ts @@ -18,3 +18,10 @@ export const parseMilliseconds = (ms: number) => { return `${leftMs}ms`; }; + +export const getTimestamp = (date?: Date | string) => { + if (!date) return 0; + if (typeof date === 'string') return new Date(date).getTime(); + + return date.getTime(); +}; From af4b653766ceb739051e606f0ce2b2e8bab9a904 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:13:54 +0200 Subject: [PATCH 05/26] feat: add pass rate for reports, report details, fix metadata display as object --- app/components/inline-stats-circle.tsx | 34 +++++++++++++++++++++ app/components/report-details/file-list.tsx | 8 +++-- app/components/reports-table.tsx | 21 ++++++++++--- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 app/components/inline-stats-circle.tsx diff --git a/app/components/inline-stats-circle.tsx b/app/components/inline-stats-circle.tsx new file mode 100644 index 00000000..f524e516 --- /dev/null +++ b/app/components/inline-stats-circle.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { FC } from 'react'; +import { CircularProgress } from '@heroui/react'; + +import { type ReportStats } from '@/app/lib/parser/types'; + +type ReportFiltersProps = { + stats: ReportStats; +}; + +const InlineStatsCircle: FC = ({ stats }) => { + if (!stats.total) return null; + + const passedPercentage = (stats.expected / (stats.total - stats.skipped)) * 100; + + return ( + + ); +}; + +export default InlineStatsCircle; diff --git a/app/components/report-details/file-list.tsx b/app/components/report-details/file-list.tsx index 91631b40..74f0c03f 100644 --- a/app/components/report-details/file-list.tsx +++ b/app/components/report-details/file-list.tsx @@ -1,11 +1,12 @@ 'use client'; import { FC, useEffect, useState } from 'react'; -import { Accordion, AccordionItem, Spinner } from '@heroui/react'; +import { Accordion, AccordionItem, Alert, Spinner } from '@heroui/react'; import { toast } from 'sonner'; import { subtitle } from '../primitives'; import { StatChart } from '../stat-chart'; +import InlineStatsCircle from '../inline-stats-circle'; import FileSuitesTree from './suite-tree'; import ReportFilters from './tests-filters'; @@ -49,13 +50,14 @@ const FileList: FC = ({ report }) => { {!filteredTests?.files?.length ? ( -

No files found

+ ) : ( - + {(filteredTests?.files ?? []).map((file) => ( } title={

{file.fileName} diff --git a/app/components/reports-table.tsx b/app/components/reports-table.tsx index 5b8d5f3b..36f01f70 100644 --- a/app/components/reports-table.tsx +++ b/app/components/reports-table.tsx @@ -21,6 +21,7 @@ import { toast } from 'sonner'; import { withBase } from '../lib/url'; import TablePaginationOptions from './table-pagination-options'; +import InlineStatsCircle from './inline-stats-circle'; import { withQueryParams } from '@/app/lib/network'; import { defaultProjectName } from '@/app/lib/constants'; @@ -33,6 +34,7 @@ import { ReadReportsHistory, ReportHistory } from '@/app/lib/storage'; const columns = [ { name: 'Title', uid: 'title' }, { name: 'Project', uid: 'project' }, + { name: 'Pass Rate', uid: 'passRate' }, { name: 'Created at', uid: 'createdAt' }, { name: 'Size', uid: 'size' }, { name: '', uid: 'actions' }, @@ -45,6 +47,7 @@ const coreFields = [ 'createdAt', 'size', 'sizeBytes', + 'options', 'reportUrl', 'metadata', 'startTime', @@ -53,6 +56,7 @@ const coreFields = [ 'projectNames', 'stats', 'errors', + 'playwrightVersion', ]; const getMetadataItems = (item: ReportHistory) => { @@ -74,6 +78,12 @@ const getMetadataItems = (item: ReportHistory) => { metadata.push({ key: 'branch', value: itemWithMetadata.branch, icon: }); } + if (itemWithMetadata.playwrightVersion) { + metadata.push({ key: 'playwright', value: itemWithMetadata.playwrightVersion }); + } + + metadata.push({ key: 'workers', value: itemWithMetadata.metadata?.actualWorkers }); + // Add any other metadata fields Object.entries(itemWithMetadata).forEach(([key, value]) => { if (!coreFields.includes(key) && !['environment', 'workingDir', 'branch'].includes(key)) { @@ -195,7 +205,7 @@ export default function ReportsTable({ onChange }: Readonly) > {(item) => ( - +

{/* Main title and link */} @@ -224,12 +234,13 @@ export default function ReportsTable({ onChange }: Readonly)
- {item.project} - + {item.project} + {} + - {item.size} - + {item.size} +
diff --git a/app/settings/components/ServerCache.tsx b/app/settings/components/ServerCache.tsx new file mode 100644 index 00000000..4c00b036 --- /dev/null +++ b/app/settings/components/ServerCache.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { Button } from '@heroui/react'; +import { toast } from 'sonner'; +import { useQueryClient } from '@tanstack/react-query'; + +import useMutation from '@/app/hooks/useMutation'; +import { invalidateCache } from '@/app/lib/query-cache'; + +interface ServerCacheProps { + isEnabled?: boolean; +} + +export default function ServerCache({ isEnabled }: ServerCacheProps) { + const queryClient = useQueryClient(); + const { + mutate: cacheRefresh, + isPending, + error, + } = useMutation('/api/cache/refresh', { + method: 'POST', + onSuccess: () => { + invalidateCache(queryClient, { queryKeys: ['/api'] }); + toast.success(`cache refreshed successfully`); + }, + }); + + return ( +
+

{isEnabled ? 'Enabled' : 'Disabled'}

+ {isEnabled && ( + + )} + {error && toast.error(error.message)} +
+ ); +} diff --git a/package-lock.json b/package-lock.json index cf4001a5..2ef098e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@react-aria/visually-hidden": "^3.8.19", "@tanstack/react-query": "^5.59.15", "@tanstack/react-query-devtools": "^5.59.15", + "better-sqlite3": "^12.4.1", "busboy": "^1.6.0", "clsx": "^2.1.1", "croner": "^9.0.0", @@ -44,6 +45,7 @@ "devDependencies": { "@cyborgtests/reporter-playwright-reports-server": "^2.3.3", "@playwright/test": "^1.55.0", + "@types/better-sqlite3": "^7.6.13", "@types/busboy": "^1.5.4", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.7", @@ -8517,6 +8519,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/busboy": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", @@ -9744,6 +9756,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz", @@ -9754,6 +9786,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/better-sqlite3": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", + "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -9766,6 +9812,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bowser": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", @@ -9837,6 +9917,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -10023,6 +10127,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -10498,6 +10608,21 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -10513,6 +10638,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10721,6 +10855,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/envalid": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz", @@ -11825,6 +11968,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -11939,6 +12091,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -12072,6 +12230,12 @@ } } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -12268,6 +12432,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -12492,6 +12662,26 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12573,6 +12763,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/input-otp": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", @@ -14312,6 +14508,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -14331,7 +14539,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14346,6 +14553,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -14396,6 +14609,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -14544,6 +14763,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -14734,7 +14965,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -15281,6 +15511,32 @@ "preact": ">=10" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -15385,6 +15641,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -15432,6 +15698,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -16152,6 +16442,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -16709,6 +17044,48 @@ "node": ">=14.0.0" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -16983,6 +17360,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -17559,7 +17948,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 848364e4..40949f5f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@react-aria/visually-hidden": "^3.8.19", "@tanstack/react-query": "^5.59.15", "@tanstack/react-query-devtools": "^5.59.15", + "better-sqlite3": "^12.4.1", "busboy": "^1.6.0", "clsx": "^2.1.1", "croner": "^9.0.0", @@ -49,6 +50,7 @@ "devDependencies": { "@cyborgtests/reporter-playwright-reports-server": "^2.3.3", "@playwright/test": "^1.55.0", + "@types/better-sqlite3": "^7.6.13", "@types/busboy": "^1.5.4", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.7", diff --git a/readme.md b/readme.md index a42060ff..30bdc58d 100644 --- a/readme.md +++ b/readme.md @@ -84,17 +84,18 @@ The Playwright Reports Server provides APIs for managing and generating reports The app is configured with environment variables, so it could be specified as `.env` file as well, however there are no mandatory options. -| Name | Description | Default | -| ---------------------- | ------------------------------------------------------------------------------------------------ | ------- | -| `API_TOKEN` | API token for [Authorization](#authorization) | | -| `AUTH_SECRET` | Secret to encrypt JWT | | -| `UI_AUTH_EXPIRE_HOURS` | Duration of auth session | `"2"` | -| `USE_SERVER_CACHE` | Use server side indexed cache for backend queries, improves UX, reduces impact on a backend / s3 | `false` | -| `DATA_STORAGE` | Where to store data, check for additional configuration [Storage Options](#storage-options) | `"fs"` | -| `JIRA_BASE_URL` | Jira instance URL (e.g., https://your-domain.atlassian.net) | | -| `JIRA_EMAIL` | Jira account email address | | -| `JIRA_API_TOKEN` | Jira API token for authentication | | -| `JIRA_PROJECT_KEY` | Default Jira project key for ticket creation | | +| Name | Description | Default | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------- | +| `API_TOKEN` | API token for [Authorization](#authorization) | | +| `AUTH_SECRET` | Secret to encrypt JWT | | +| `UI_AUTH_EXPIRE_HOURS` | Duration of auth session | `"2"` | +| `USE_SERVER_CACHE` | Use sqlite3 for storing metadata for results and reports, config caching - improves UX, reduces impact on a s3/fs | `false` | +| `SERVER_CACHE_REFRESH_CRON` | | | +| `DATA_STORAGE` | Where to store data, check for additional configuration [Storage Options](#storage-options) | `"fs"` | +| `JIRA_BASE_URL` | Jira instance URL (e.g., https://your-domain.atlassian.net) | | +| `JIRA_EMAIL` | Jira account email address | | +| `JIRA_API_TOKEN` | Jira API token for authentication | | +| `JIRA_PROJECT_KEY` | Default Jira project key for ticket creation | | ## API Routes From 4ecd681336b98ed86546e5f99b39b2ef881d7a96 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:05:17 +0200 Subject: [PATCH 09/26] fix: download only zip for report generation chore: test upload chore: remove auth for upload test chore: remove bs --- app/lib/storage/s3.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index bae16d5a..39bcb9ab 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -895,7 +895,7 @@ export class S3 implements Storage { const id = fileName.replace(path.extname(fileName), ''); - if (resultsIds.includes(id)) { + if (resultsIds.includes(id) && path.extname(fileName) === '.zip') { console.log(`[s3] file id is in target results, downloading...`); const localFilePath = path.join(tempFolder, fileName); From c3da05f5604a577a1b0444825b7fe0e2f2f63f3c Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Wed, 19 Nov 2025 23:51:19 +0200 Subject: [PATCH 10/26] feat: pause stream when uploading chunk --- app/lib/storage/s3.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index 39bcb9ab..9b891d4b 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -702,6 +702,8 @@ export class S3 implements Storage { `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, ); + stream.pause(); + const uploadPartResult = await this.client.send( new UploadPartCommand({ Bucket: this.bucket, @@ -712,6 +714,8 @@ export class S3 implements Storage { }), ); + stream.resume(); + if (!uploadPartResult.ETag) { throw new Error(`[s3] failed to upload part ${partNumber}: no ETag received`); } From b87ae29e8c25420587a0efbdffd03c65f19452e3 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:11:44 +0200 Subject: [PATCH 11/26] feat: try manual stream backpressure to stop reading when upload in progress --- app/lib/storage/s3.ts | 1 + pages/api/result/upload.ts | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index 9b891d4b..f108b782 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -714,6 +714,7 @@ export class S3 implements Storage { }), ); + console.log(`[s3] uploaded part ${partNumber}, resume reading`); stream.resume(); if (!uploadPartResult.ETag) { diff --git a/pages/api/result/upload.ts b/pages/api/result/upload.ts index 5013a8c3..1d723ef1 100644 --- a/pages/api/result/upload.ts +++ b/pages/api/result/upload.ts @@ -71,12 +71,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) reject(error); }); - pipeline( - fileStream.on('data', (chunk: Buffer) => { - fileSize += chunk.length; - }), - filePassThrough, - ).catch((e) => { + fileStream.on('data', (chunk: Buffer) => { + fileSize += chunk.length; + + if (!filePassThrough.write(chunk)) { + fileStream.pause(); + + filePassThrough.once('drain', () => { + fileStream.resume(); + }); + } + }); + + fileStream.on('end', () => filePassThrough.end()); + fileStream.on('error', (e) => { filePassThrough.destroy(e); reject(e); }); From 01acf98627b97ec433057304de721e03c94af289 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:46:41 +0200 Subject: [PATCH 12/26] feat: explicitly stop file buffer when passthrough is stopped --- pages/api/result/upload.ts | 59 +++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/pages/api/result/upload.ts b/pages/api/result/upload.ts index 1d723ef1..61771e15 100644 --- a/pages/api/result/upload.ts +++ b/pages/api/result/upload.ts @@ -12,6 +12,35 @@ import { withError } from '@/app/lib/withError'; export const config = { api: { bodyParser: false } }; +async function waitForBufferDrain(stream: PassThrough, maxWaitMs = 10000): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const checkBuffer = () => { + const buffered = stream.readableLength || 0; + + if (buffered === 0) { + console.log('[upload] buffer is empty'); + resolve(); + + return; + } + + if (Date.now() - startTime > maxWaitMs) { + reject(new Error(`Timeout waiting for buffer to drain (${buffered} bytes remaining)`)); + + return; + } + + console.log(`[upload] waiting for buffer to drain (${buffered} bytes remaining)`); + + setTimeout(checkBuffer, 100); + }; + + checkBuffer(); + }); +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'PUT') { res.setHeader('Allow', 'PUT'); @@ -64,6 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) bb.on('file', (_, fileStream) => { fileReceived = true; + let isPaused = false; saveResultPromise = service .saveResult(fileName, filePassThrough, presignedUrl, contentLength) @@ -74,12 +104,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) fileStream.on('data', (chunk: Buffer) => { fileSize += chunk.length; - if (!filePassThrough.write(chunk)) { + const canContinue = filePassThrough.write(chunk); + + if (!canContinue && !isPaused) { + isPaused = true; fileStream.pause(); + req.pause(); + console.log('[upload] PAUSED - buffer full'); + console.log(` - fileStream buffered: ${fileStream.readableLength} bytes`); + console.log(` - PassThrough buffered: ${filePassThrough.readableLength} bytes`); + console.log(` - PassThrough writable buffer: ${filePassThrough.writableLength} bytes`); + } + }); + + filePassThrough.on('drain', async () => { + if (isPaused) { + const fileStreamBuffered = fileStream.readableLength || 0; + const passThroughBuffered = filePassThrough.readableLength || 0; + + console.log('[upload] Drain event received'); + console.log(` - fileStream buffered: ${fileStreamBuffered} bytes`); + console.log(` - PassThrough buffered: ${passThroughBuffered} bytes`); + await waitForBufferDrain(filePassThrough); + isPaused = false; + fileStream.resume(); + req.resume(); - filePassThrough.once('drain', () => { - fileStream.resume(); - }); + console.log('[upload] RESUMED - buffer drained'); } }); From 8c118e843572e6453fd256a44ec7a0d80e7d0a59 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:03:37 +0200 Subject: [PATCH 13/26] feat: upload - cleanup streams, avoid copying buffers, ensure chunks cleared --- app/lib/storage/s3.ts | 53 ++++++++++++++++++++++++++++++++------ pages/api/result/upload.ts | 39 +++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index f108b782..a9c783d6 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -687,20 +687,42 @@ export class S3 implements Storage { const uploadedParts: { PartNumber: number; ETag: string }[] = []; let partNumber = 1; - let buffer = Buffer.alloc(0); + const chunks: Buffer[] = []; + let currentSize = 0; try { for await (const chunk of stream) { - buffer = Buffer.concat([buffer, chunk]); + console.log(`[s3] received chunk of size ${(chunk.length / (1024 * 1024)).toFixed(2)}MB for ${filename}`); - while (buffer.length >= chunkSize) { - const partData = buffer.subarray(0, chunkSize); + chunks.push(chunk); + currentSize += chunk.length; + + while (currentSize >= chunkSize) { + const partData = Buffer.allocUnsafe(chunkSize); + let copied = 0; + + while (copied < chunkSize && chunks.length > 0) { + const currentChunk = chunks[0]; + const needed = chunkSize - copied; + const available = currentChunk.length; + + if (available <= needed) { + currentChunk.copy(partData, copied); + copied += available; + chunks.shift(); + } else { + currentChunk.copy(partData, copied, 0, needed); + copied += needed; + chunks[0] = currentChunk.subarray(needed); + } + } - buffer = buffer.subarray(chunkSize); + currentSize -= chunkSize; console.log( `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, ); + console.log(`[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`); stream.pause(); @@ -714,6 +736,9 @@ export class S3 implements Storage { }), ); + // explicitly clear the part data to help GC + partData.fill(0); + console.log(`[s3] uploaded part ${partNumber}, resume reading`); stream.resume(); @@ -730,8 +755,16 @@ export class S3 implements Storage { } } - if (buffer.length > 0) { - console.log(`[s3] uploading final part ${partNumber} [${bytesToString(buffer.length)}] for ${filename}`); + if (currentSize > 0) { + console.log(`[s3] uploading final part ${partNumber} [${bytesToString(currentSize)}] for ${filename}`); + + const finalPart = Buffer.allocUnsafe(currentSize); + let offset = 0; + + for (const chunk of chunks) { + chunk.copy(finalPart, offset); + offset += chunk.length; + } const uploadPartResult = await this.client.send( new UploadPartCommand({ @@ -739,10 +772,14 @@ export class S3 implements Storage { Key: remotePath, UploadId: uploadID, PartNumber: partNumber, - Body: buffer, + Body: finalPart, }), ); + // explicitly clear buffer references + chunks.length = 0; + finalPart.fill(0); + if (!uploadPartResult.ETag) { throw new Error(`[s3] failed to upload final part ${partNumber}: no ETag received`); } diff --git a/pages/api/result/upload.ts b/pages/api/result/upload.ts index 61771e15..53f7fd00 100644 --- a/pages/api/result/upload.ts +++ b/pages/api/result/upload.ts @@ -81,13 +81,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const uploadPromise = new Promise((resolve, reject) => { let fileReceived = false; + let cleanupDone = false; - const onAborted = () => { + const cleanup = () => { + if (cleanupDone) return; + cleanupDone = true; + + console.log('[upload] cleaning up streams'); + + // remove all listeners to prevent memory leaks + req.removeAllListeners('aborted'); + req.removeAllListeners('close'); + + // explicitly destroy streams if (!filePassThrough.destroyed) { - filePassThrough.destroy(new Error('Client aborted connection')); + filePassThrough.destroy(); } }; + const onAborted = () => { + console.log('[upload] request aborted or closed'); + cleanup(); + reject(new Error('Client aborted connection')); + }; + req.on('aborted', onAborted); req.on('close', onAborted); @@ -98,6 +115,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) saveResultPromise = service .saveResult(fileName, filePassThrough, presignedUrl, contentLength) .catch((error: Error) => { + cleanup(); reject(error); }); @@ -134,9 +152,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); - fileStream.on('end', () => filePassThrough.end()); + fileStream.on('end', () => { + console.log('[upload] fileStream ended'); + filePassThrough.end(); + }); + fileStream.on('error', (e) => { - filePassThrough.destroy(e); + console.error('[upload] fileStream error:', e); + cleanup(); reject(e); }); }); @@ -146,11 +169,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); bb.on('error', (error: Error) => { + console.error('[upload] busboy error:', error); + cleanup(); reject(error); }); bb.on('finish', async () => { if (!fileReceived) { + cleanup(); reject(new Error('No file received')); return; @@ -160,19 +186,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { error } = await withError(saveResultPromise); if (error) { + cleanup(); reject(error); + + return; } if (contentLength) { const expected = parseInt(contentLength, 10); if (Number.isFinite(expected) && expected > 0 && fileSize !== expected) { + cleanup(); reject(new Error(`Size mismatch: received ${fileSize} bytes, expected ${expected} bytes`)); return; } } + cleanup(); resolve(); } }); From 6cdd1f834b8ce7a5f87b24e1ea84e32710d59ef5 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:55:11 +0200 Subject: [PATCH 14/26] chore: upload - increase wait stream timeout and interval --- pages/api/result/upload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/result/upload.ts b/pages/api/result/upload.ts index 53f7fd00..9ebb21b5 100644 --- a/pages/api/result/upload.ts +++ b/pages/api/result/upload.ts @@ -12,7 +12,7 @@ import { withError } from '@/app/lib/withError'; export const config = { api: { bodyParser: false } }; -async function waitForBufferDrain(stream: PassThrough, maxWaitMs = 10000): Promise { +async function waitForBufferDrain(stream: PassThrough, maxWaitMs = 30 * 1000): Promise { const startTime = Date.now(); return new Promise((resolve, reject) => { @@ -34,7 +34,7 @@ async function waitForBufferDrain(stream: PassThrough, maxWaitMs = 10000): Promi console.log(`[upload] waiting for buffer to drain (${buffered} bytes remaining)`); - setTimeout(checkBuffer, 100); + setTimeout(checkBuffer, 250); }; checkBuffer(); From 1263594e5de06927a2d10b0c1fe820b659f503b0 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:19:21 +0200 Subject: [PATCH 15/26] feat: upload - stop reading req when stream full and re-pipe on resume --- pages/api/result/upload.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pages/api/result/upload.ts b/pages/api/result/upload.ts index 9ebb21b5..a05df5b4 100644 --- a/pages/api/result/upload.ts +++ b/pages/api/result/upload.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { pipeline } from 'node:stream/promises'; import { PassThrough } from 'node:stream'; import { randomUUID } from 'node:crypto'; @@ -89,6 +88,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.log('[upload] cleaning up streams'); + // stop reading from req + req.unpipe(bb); + // remove all listeners to prevent memory leaks req.removeAllListeners('aborted'); req.removeAllListeners('close'); @@ -127,8 +129,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!canContinue && !isPaused) { isPaused = true; fileStream.pause(); - req.pause(); - console.log('[upload] PAUSED - buffer full'); + req.unpipe(bb); + console.log('[upload] PAUSED - buffer full, stop reading req'); console.log(` - fileStream buffered: ${fileStream.readableLength} bytes`); console.log(` - PassThrough buffered: ${filePassThrough.readableLength} bytes`); console.log(` - PassThrough writable buffer: ${filePassThrough.writableLength} bytes`); @@ -145,10 +147,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.log(` - PassThrough buffered: ${passThroughBuffered} bytes`); await waitForBufferDrain(filePassThrough); isPaused = false; + req.pipe(bb); fileStream.resume(); - req.resume(); - console.log('[upload] RESUMED - buffer drained'); + console.log('[upload] RESUMED - buffer drained, continue reading req'); } }); @@ -209,13 +211,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); }); - const { error: streamPipelineError } = await withError(pipeline(req, bb)); - - if (streamPipelineError) { - console.error('Error processing request:', streamPipelineError); - - return; - } + req.pipe(bb); const { error: uploadError } = await withError(uploadPromise); From 053ebdebe17d18f5f3f05129de4651ec6acf2fcb Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:58:51 +0200 Subject: [PATCH 16/26] fix: remove remaining config db leftovers, plain cache object is used --- app/lib/service/db/db.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/lib/service/db/db.ts b/app/lib/service/db/db.ts index 186a68da..2adfa6ca 100644 --- a/app/lib/service/db/db.ts +++ b/app/lib/service/db/db.ts @@ -129,7 +129,6 @@ export function closeDatabase(): void { export function getDatabaseStats(): { resultsCount: number; reportsCount: number; - configExists: boolean; dbSizeOnDisk: string; estimatedRAM: string; } { @@ -137,7 +136,6 @@ export function getDatabaseStats(): { const resultsCount = db.prepare('SELECT COUNT(*) as count FROM results').get() as { count: number }; const reportsCount = db.prepare('SELECT COUNT(*) as count FROM reports').get() as { count: number }; - const configExists = db.prepare('SELECT COUNT(*) as count FROM config').get() as { count: number }; const stats = { pageCount: db.pragma('page_count', { simple: true }) as number, @@ -151,7 +149,6 @@ export function getDatabaseStats(): { return { resultsCount: resultsCount.count, reportsCount: reportsCount.count, - configExists: configExists.count > 0, dbSizeOnDisk: `${(dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, estimatedRAM: `${(cacheSizeBytes / 1024 / 1024).toFixed(2)} MB`, }; @@ -165,7 +162,6 @@ export function clearAll(): void { db.exec(` DELETE FROM results; DELETE FROM reports; - DELETE FROM config; DELETE FROM cache_metadata; `); From 0fbbc790b3ca2df35777447e42547c82b247b003 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:13:51 +0200 Subject: [PATCH 17/26] feat: display db stats in settings --- app/api/config/route.ts | 3 +- app/lib/service/db/db.ts | 14 +++--- app/settings/components/DatabaseInfo.tsx | 48 +++++++++++++++++++++ app/settings/components/EnvironmentInfo.tsx | 10 ++--- app/settings/components/ServerCache.tsx | 46 -------------------- app/types/index.ts | 11 ++++- 6 files changed, 69 insertions(+), 63 deletions(-) create mode 100644 app/settings/components/DatabaseInfo.tsx delete mode 100644 app/settings/components/ServerCache.tsx diff --git a/app/api/config/route.ts b/app/api/config/route.ts index d5347380..16aa5967 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -9,6 +9,7 @@ import { service } from '@/app/lib/service'; import { JiraService } from '@/app/lib/service/jira'; import { env } from '@/app/config/env'; import { cronService } from '@/app/lib/service/cron'; +import { getDatabaseStats } from '@/app/lib/service/db'; export const dynamic = 'force-dynamic'; // defaults to auto @@ -177,7 +178,7 @@ export async function GET() { // Add environment info to config response const envInfo = { authRequired: !!env.API_TOKEN, - serverCache: env.USE_SERVER_CACHE, + database: getDatabaseStats(), dataStorage: env.DATA_STORAGE, s3Endpoint: env.S3_ENDPOINT, s3Bucket: env.S3_BUCKET, diff --git a/app/lib/service/db/db.ts b/app/lib/service/db/db.ts index 2adfa6ca..21216ad3 100644 --- a/app/lib/service/db/db.ts +++ b/app/lib/service/db/db.ts @@ -127,9 +127,9 @@ export function closeDatabase(): void { } export function getDatabaseStats(): { - resultsCount: number; - reportsCount: number; - dbSizeOnDisk: string; + results: number; + reports: number; + sizeOnDisk: string; estimatedRAM: string; } { const db = getDatabase(); @@ -147,10 +147,10 @@ export function getDatabaseStats(): { const cacheSizeBytes = Math.abs(stats.cacheSize) * (stats.cacheSize < 0 ? 1024 : stats.pageSize); return { - resultsCount: resultsCount.count, - reportsCount: reportsCount.count, - dbSizeOnDisk: `${(dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, - estimatedRAM: `${(cacheSizeBytes / 1024 / 1024).toFixed(2)} MB`, + results: resultsCount.count, + reports: reportsCount.count, + sizeOnDisk: `${(dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, + estimatedRAM: `~${(cacheSizeBytes / 1024 / 1024).toFixed(2)} MB`, }; } diff --git a/app/settings/components/DatabaseInfo.tsx b/app/settings/components/DatabaseInfo.tsx new file mode 100644 index 00000000..7f3ff217 --- /dev/null +++ b/app/settings/components/DatabaseInfo.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { Button } from '@heroui/react'; +import { toast } from 'sonner'; +import { useQueryClient } from '@tanstack/react-query'; + +import useMutation from '@/app/hooks/useMutation'; +import { invalidateCache } from '@/app/lib/query-cache'; +import { DatabaseStats } from '@/app/types'; + +interface DatabaseInfoProps { + stats?: DatabaseStats; +} + +export default function DatabaseInfo({ stats }: Readonly) { + const queryClient = useQueryClient(); + const { + mutate: cacheRefresh, + isPending, + error, + } = useMutation('/api/cache/refresh', { + method: 'POST', + onSuccess: () => { + invalidateCache(queryClient, { queryKeys: ['/api'] }); + toast.success(`db refreshed successfully`); + }, + }); + + return ( +
+

Size: {stats?.sizeOnDisk ?? 'n/a'}

+

RAM: {stats?.estimatedRAM}

+

Results: {stats?.results}

+

Reports: {stats?.reports}

+ + {error && toast.error(error.message)} +
+ ); +} diff --git a/app/settings/components/EnvironmentInfo.tsx b/app/settings/components/EnvironmentInfo.tsx index f2427f78..4cda1662 100644 --- a/app/settings/components/EnvironmentInfo.tsx +++ b/app/settings/components/EnvironmentInfo.tsx @@ -2,7 +2,7 @@ import { Card, CardBody, CardHeader, Skeleton } from '@heroui/react'; -import ServerCache from './ServerCache'; +import DatabaseInfo from './DatabaseInfo'; import { useAuthConfig } from '@/app/hooks/useAuthConfig'; @@ -25,12 +25,8 @@ export default function EnvironmentInfo() { )}
- Server Cache - {isLoading ? ( - - ) : ( - - )} + Database + {isLoading ? : }
Data Storage diff --git a/app/settings/components/ServerCache.tsx b/app/settings/components/ServerCache.tsx deleted file mode 100644 index 4c00b036..00000000 --- a/app/settings/components/ServerCache.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { Button } from '@heroui/react'; -import { toast } from 'sonner'; -import { useQueryClient } from '@tanstack/react-query'; - -import useMutation from '@/app/hooks/useMutation'; -import { invalidateCache } from '@/app/lib/query-cache'; - -interface ServerCacheProps { - isEnabled?: boolean; -} - -export default function ServerCache({ isEnabled }: ServerCacheProps) { - const queryClient = useQueryClient(); - const { - mutate: cacheRefresh, - isPending, - error, - } = useMutation('/api/cache/refresh', { - method: 'POST', - onSuccess: () => { - invalidateCache(queryClient, { queryKeys: ['/api'] }); - toast.success(`cache refreshed successfully`); - }, - }); - - return ( -
-

{isEnabled ? 'Enabled' : 'Disabled'}

- {isEnabled && ( - - )} - {error && toast.error(error.message)} -
- ); -} diff --git a/app/types/index.ts b/app/types/index.ts index 4e9c2265..733af8d5 100644 --- a/app/types/index.ts +++ b/app/types/index.ts @@ -22,7 +22,7 @@ export interface SiteWhiteLabelConfig { faviconPath: string; reporterPaths?: string[]; authRequired?: boolean; - serverCache?: boolean; + database?: DatabaseStats; dataStorage?: string; s3Endpoint?: string; s3Bucket?: string; @@ -35,9 +35,16 @@ export interface SiteWhiteLabelConfig { jira?: JiraConfig; } +export interface DatabaseStats { + sizeOnDisk: string; + estimatedRAM: string; + reports: number; + results: number; +} + export interface EnvInfo { authRequired: boolean; - serverCache: boolean | undefined; + database: DatabaseStats; dataStorage: string | undefined; s3Endpoint: string | undefined; s3Bucket: string | undefined; From 991a0ca5036212afee977ba8466b796be4989893 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:15:39 +0200 Subject: [PATCH 18/26] feat: enable db, move filters and pagination to sql --- app/api/cache/refresh/route.ts | 6 +- app/config/env.ts | 3 +- app/lib/service/cache/config.ts | 7 +- app/lib/service/db/reports.sqlite.ts | 84 +++++++++++---- app/lib/service/db/results.sqlite.ts | 93 ++++++++++++---- app/lib/service/index.ts | 154 ++++----------------------- app/lib/service/lifecycle.ts | 7 +- app/lib/storage/fs.ts | 151 +++++++------------------- app/lib/storage/pagination.ts | 12 +-- app/lib/storage/s3.ts | 129 +++------------------- app/lib/storage/types.ts | 11 +- readme.md | 23 ++-- 12 files changed, 241 insertions(+), 439 deletions(-) diff --git a/app/api/cache/refresh/route.ts b/app/api/cache/refresh/route.ts index c50ae9ce..c6b86405 100644 --- a/app/api/cache/refresh/route.ts +++ b/app/api/cache/refresh/route.ts @@ -2,14 +2,14 @@ import { revalidatePath } from 'next/cache'; import { withError } from '@/app/lib/withError'; import { forceInitDatabase } from '@/app/lib/service/db'; -import { env } from '@/app/config/env'; import { configCache } from '@/app/lib/service/cache/config'; +import { lifecycle } from '@/app/lib/service/lifecycle'; export const dynamic = 'force-dynamic'; // defaults to auto export async function POST(_: Request) { - if (!env.USE_SERVER_CACHE) { - return Response.json({ error: 'USE_SERVER_CACHE is disabled' }, { status: 403 }); + if (!lifecycle.isInitialized()) { + return Response.json({ error: 'service is not initialized' }, { status: 500 }); } configCache.initialized = false; diff --git a/app/config/env.ts b/app/config/env.ts index d2a9795e..73331999 100644 --- a/app/config/env.ts +++ b/app/config/env.ts @@ -1,11 +1,10 @@ -import { cleanEnv, str, num, bool } from 'envalid'; +import { cleanEnv, str, num } from 'envalid'; export const env = cleanEnv(process.env, { API_TOKEN: str({ desc: 'API token for authorization', default: undefined }), UI_AUTH_EXPIRE_HOURS: str({ desc: 'How much hours are allowed to keep auth session valid', default: '2' }), AUTH_SECRET: str({ desc: 'Secret for JWT', default: undefined }), DATA_STORAGE: str({ desc: 'Where to store data', default: 'fs' }), - USE_SERVER_CACHE: bool({ desc: 'Use sqlite for metadata storing, cache config', default: false }), S3_ENDPOINT: str({ desc: 'S3 endpoint', default: undefined }), S3_ACCESS_KEY: str({ desc: 'S3 access key', default: undefined }), S3_SECRET_KEY: str({ desc: 'S3 secret key', default: undefined }), diff --git a/app/lib/service/cache/config.ts b/app/lib/service/cache/config.ts index cdbd8946..3e5679cf 100644 --- a/app/lib/service/cache/config.ts +++ b/app/lib/service/cache/config.ts @@ -1,5 +1,4 @@ import { storage } from '@/app/lib/storage'; -import { env } from '@/app/config/env'; import { SiteWhiteLabelConfig } from '@/app/types'; import { defaultConfig } from '@/app/lib/config'; @@ -19,7 +18,7 @@ export class ConfigCache { } public async init(): Promise { - if (this.initialized || !env.USE_SERVER_CACHE) { + if (this.initialized) { return; } @@ -42,10 +41,6 @@ export class ConfigCache { } public onChanged(config: SiteWhiteLabelConfig) { - if (!env.USE_SERVER_CACHE) { - return; - } - this.config = config; } } diff --git a/app/lib/service/db/reports.sqlite.ts b/app/lib/service/db/reports.sqlite.ts index af1c83c4..c8274e5b 100644 --- a/app/lib/service/db/reports.sqlite.ts +++ b/app/lib/service/db/reports.sqlite.ts @@ -5,13 +5,12 @@ import { withError } from '../../withError'; import { getDatabase } from './db'; import { storage } from '@/app/lib/storage'; -import { type ReportHistory } from '@/app/lib/storage/types'; -import { env } from '@/app/config/env'; +import { type ReportHistory, type ReadReportsInput, type ReadReportsOutput } from '@/app/lib/storage/types'; const initiatedReportsDb = Symbol.for('playwright.reports.db.reports'); -const instance = globalThis as typeof globalThis & { [initiatedReportsDb]?: ReportCache }; +const instance = globalThis as typeof globalThis & { [initiatedReportsDb]?: ReportDatabase }; -export class ReportCache { +export class ReportDatabase { public initialized = false; private readonly db = getDatabase(); @@ -54,14 +53,14 @@ export class ReportCache { `); } - public static getInstance() { - instance[initiatedReportsDb] ??= new ReportCache(); + public static getInstance(): ReportDatabase { + instance[initiatedReportsDb] ??= new ReportDatabase(); return instance[initiatedReportsDb]; } public async init() { - if (this.initialized || !env.USE_SERVER_CACHE) { + if (this.initialized) { return; } @@ -112,10 +111,6 @@ export class ReportCache { } public onDeleted(reportIds: string[]) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[report db] deleting ${reportIds.length} reports`); const deleteMany = this.db.transaction((ids: string[]) => { @@ -128,19 +123,11 @@ export class ReportCache { } public onCreated(report: ReportHistory) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[report db] adding report ${report.reportID}`); this.insertReport(report); } public onUpdated(report: ReportHistory) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[report db] updating report ${report.reportID}`); const { reportID, project, title, reportUrl, size, sizeBytes, stats, ...metadata } = report; @@ -234,6 +221,63 @@ export class ReportCache { this.db.prepare('DELETE FROM reports').run(); } + public query(input?: ReadReportsInput): ReadReportsOutput { + let query = 'SELECT * FROM reports'; + const params: string[] = []; + const conditions: string[] = []; + + if (input?.ids && input.ids.length > 0) { + conditions.push(`reportID IN (${input.ids.map(() => '?').join(', ')})`); + params.push(...input.ids); + } + + if (input?.project) { + conditions.push('project = ?'); + params.push(input.project); + } + + if (input?.search?.trim()) { + const searchTerm = `%${input.search.toLowerCase().trim()}%`; + + conditions.push( + '(LOWER(title) LIKE ? OR LOWER(reportID) LIKE ? OR LOWER(project) LIKE ? OR LOWER(metadata) LIKE ?)', + ); + params.push(searchTerm, searchTerm, searchTerm, searchTerm); + } + + if (conditions.length > 0) { + query += ' WHERE ' + conditions.join(' AND '); + } + + query += ' ORDER BY createdAt DESC'; + + const countQuery = query.replace('SELECT *', 'SELECT COUNT(*) as count'); + const countResult = this.db.prepare(countQuery).get(...params) as { count: number }; + const total = countResult.count; + + if (input?.pagination) { + query += ' LIMIT ? OFFSET ?'; + params.push(input.pagination.limit.toString(), input.pagination.offset.toString()); + } + + const rows = this.db.prepare(query).all(...params) as Array<{ + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }>; + + return { + reports: rows.map((row) => this.rowToReport(row)), + total, + }; + } + private rowToReport(row: { reportID: string; project: string; @@ -262,4 +306,4 @@ export class ReportCache { } } -export const reportDb = ReportCache.getInstance(); +export const reportDb = ReportDatabase.getInstance(); diff --git a/app/lib/service/db/results.sqlite.ts b/app/lib/service/db/results.sqlite.ts index a97ae44d..47b76869 100644 --- a/app/lib/service/db/results.sqlite.ts +++ b/app/lib/service/db/results.sqlite.ts @@ -5,13 +5,12 @@ import { withError } from '../../withError'; import { getDatabase } from './db'; import { storage } from '@/app/lib/storage'; -import { type Result } from '@/app/lib/storage/types'; -import { env } from '@/app/config/env'; +import { type Result, type ReadResultsInput, type ReadResultsOutput } from '@/app/lib/storage/types'; const initiatedResultsDb = Symbol.for('playwright.reports.db.results'); -const instance = globalThis as typeof globalThis & { [initiatedResultsDb]?: ResultCache }; +const instance = globalThis as typeof globalThis & { [initiatedResultsDb]?: ResultDatabase }; -export class ResultCache { +export class ResultDatabase { public initialized = false; private readonly db = getDatabase(); @@ -52,14 +51,14 @@ export class ResultCache { `); } - public static getInstance() { - instance[initiatedResultsDb] ??= new ResultCache(); + public static getInstance(): ResultDatabase { + instance[initiatedResultsDb] ??= new ResultDatabase(); return instance[initiatedResultsDb]; } public async init() { - if (this.initialized || !env.USE_SERVER_CACHE) { + if (this.initialized) { return; } @@ -108,10 +107,6 @@ export class ResultCache { } public onDeleted(resultIds: string[]) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[result db] deleting ${resultIds.length} results`); const deleteMany = this.db.transaction((ids: string[]) => { @@ -124,19 +119,11 @@ export class ResultCache { } public onCreated(result: Result) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[result db] adding result ${result.resultID}`); this.insertResult(result); } public onUpdated(result: Result) { - if (!env.USE_SERVER_CACHE) { - return; - } - console.log(`[result db] updating result ${result.resultID}`); const { resultID, project, title, size, sizeBytes, ...metadata } = result; @@ -213,6 +200,72 @@ export class ResultCache { this.db.prepare('DELETE FROM results').run(); } + public query(input?: ReadResultsInput): ReadResultsOutput { + let query = 'SELECT * FROM results'; + const params: string[] = []; + const conditions: string[] = []; + + if (input?.project) { + conditions.push('project = ?'); + params.push(input.project); + } + + if (input?.testRun) { + conditions.push('metadata LIKE ?'); + params.push(`%"testRun":"${input.testRun}"%`); + } + + if (input?.tags && input.tags.length > 0) { + console.log('Filtering by tags:', input.tags); + + for (const tag of input.tags) { + const [key, value] = tag.split(':').map((part) => part.trim()); + + conditions.push('metadata LIKE ?'); + params.push(`%"${key}":"${value}"%`); + } + } + + if (input?.search?.trim()) { + const searchTerm = `%${input.search.toLowerCase().trim()}%`; + + conditions.push( + '(LOWER(title) LIKE ? OR LOWER(resultID) LIKE ? OR LOWER(project) LIKE ? OR LOWER(metadata) LIKE ?)', + ); + params.push(searchTerm, searchTerm, searchTerm, searchTerm); + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(' AND ')}`; + } + + query += ' ORDER BY createdAt DESC'; + + const countQuery = query.replace('SELECT *', 'SELECT COUNT(*) as count'); + const countResult = this.db.prepare(countQuery).get(...params) as { count: number }; + const total = countResult.count; + + if (input?.pagination) { + query += ' LIMIT ? OFFSET ?'; + params.push(input.pagination.limit.toString(), input.pagination.offset.toString()); + } + + const rows = this.db.prepare(query).all(...params) as Array<{ + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }>; + + return { + results: rows.map((row) => this.rowToResult(row)), + total, + }; + } + private rowToResult(row: { resultID: string; project: string; @@ -236,4 +289,4 @@ export class ResultCache { } } -export const resultDb = ResultCache.getInstance(); +export const resultDb = ResultDatabase.getInstance(); diff --git a/app/lib/service/index.ts b/app/lib/service/index.ts index b319a026..f98b2f6c 100644 --- a/app/lib/service/index.ts +++ b/app/lib/service/index.ts @@ -12,20 +12,17 @@ import { type ReadReportsInput, ReadResultsInput, ReadResultsOutput, - ReportHistory, ReportMetadata, + ReportPath, ResultDetails, ServerDataInfo, - isReportHistory, storage, } from '@/app/lib/storage'; -import { handlePagination } from '@/app/lib/storage/pagination'; import { SiteWhiteLabelConfig } from '@/app/types'; import { defaultConfig } from '@/app/lib/config'; import { env } from '@/app/config/env'; import { type S3 } from '@/app/lib/storage/s3'; import { isValidPlaywrightVersion } from '@/app/lib/pw'; -import { getTimestamp } from '@/app/lib/time'; const runningService = Symbol.for('playwright.reports.service'); const instance = globalThis as typeof globalThis & { [runningService]?: Service }; @@ -38,83 +35,18 @@ class Service { return instance[runningService]; } - private shouldUseServerCache(): boolean { - return env.USE_SERVER_CACHE && lifecycle.isInitialized(); - } - public async getReports(input?: ReadReportsInput) { console.log(`[service] getReports`); - const cached = this.shouldUseServerCache() && reportDb.initialized ? reportDb.getAll() : []; - - const shouldUseCache = !input?.ids; - - if (cached.length && shouldUseCache) { - console.log(`[service] using cached reports`); - const noFilters = !input?.project && !input?.ids; - const shouldFilterByProject = (report: ReportHistory) => input?.project && report.project === input.project; - const shouldFilterByID = (report: ReportHistory) => input?.ids?.includes(report.reportID); - - let reports = cached.filter((report) => noFilters || shouldFilterByProject(report) || shouldFilterByID(report)); - - // Filter by search if provided - if (input?.search?.trim()) { - const searchTerm = input.search.toLowerCase().trim(); - - reports = reports.filter((report) => { - // Search in title, reportID, project, and all metadata fields - const searchableFields = [ - report.title, - report.reportID, - report.project, - ...Object.entries(report) - .filter( - ([key]) => - !['reportID', 'title', 'createdAt', 'size', 'sizeBytes', 'project', 'reportUrl', 'stats'].includes( - key, - ), - ) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - reports.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); - const currentReports = handlePagination(reports, input?.pagination); - - return { - reports: currentReports, - total: reports.length, - }; - } - - console.log(`[service] using external reports`); - - return await storage.readReports(input); + return reportDb.query(input); } - public async getReport(id: string): Promise { + public async getReport(id: string) { console.log(`[service] getReport ${id}`); - const cached = this.shouldUseServerCache() && reportDb.initialized ? reportDb.getByID(id) : undefined; - - if (isReportHistory(cached)) { - console.log(`[service] using cached report`); - - return cached; - } - - console.log(`[service] fetching report`); - - const { reports } = await this.getReports({ ids: [id] }); - const report = reports.find((report) => report.reportID === id); - - if (!report) { - throw new Error(`report with id ${id} not found`); - } + const report = reportDb.getByID(id); - return report; + return report!; } private async findLatestPlaywrightVersionFromResults(resultIds: string[]) { @@ -185,7 +117,15 @@ class Service { } public async deleteReports(reportIDs: string[]) { - const { error } = await withError(storage.deleteReports(reportIDs)); + const entries: ReportPath[] = []; + + for (const id of reportIDs) { + const report = await this.getReport(id); + + entries.push({ reportID: id, project: report.project }); + } + + const { error } = await withError(storage.deleteReports(entries)); if (error) { throw error; @@ -203,60 +143,8 @@ class Service { public async getResults(input?: ReadResultsInput): Promise { console.log(`[results service] getResults`); - const cached = this.shouldUseServerCache() && resultDb.initialized ? resultDb.getAll() : []; - if (!cached.length) { - return await storage.readResults(input); - } - - cached.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); - - let filtered = input?.project - ? cached.filter((file) => (input?.project ? file.project === input.project : file)) - : cached; - - if (input?.testRun) { - filtered = filtered.filter((file) => file.testRun === input.testRun); - } - - // Filter by tags if provided - if (input?.tags && input.tags.length > 0) { - const notMetadataKeys = ['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project']; - - filtered = filtered.filter((result) => { - const resultTags = Object.entries(result) - .filter(([key]) => !notMetadataKeys.includes(key)) - .map(([key, value]) => `${key}: ${value}`); - - return input.tags!.some((selectedTag) => resultTags.includes(selectedTag)); - }); - } - - // Filter by search if provided - if (input?.search?.trim()) { - const searchTerm = input.search.toLowerCase().trim(); - - filtered = filtered.filter((result) => { - // Search in title, resultID, project, and all metadata fields - const searchableFields = [ - result.title, - result.resultID, - result.project, - ...Object.entries(result) - .filter(([key]) => !['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project'].includes(key)) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - - const results = !input?.pagination ? filtered : handlePagination(filtered, input?.pagination); - - return { - results, - total: filtered.length, - }; + return resultDb.query(input); } public async deleteResults(resultIDs: string[]): Promise { @@ -358,7 +246,7 @@ class Service { public async getServerInfo(): Promise { console.log(`[service] getServerInfo`); - const canCalculateFromCache = this.shouldUseServerCache() && reportDb.initialized && resultDb.initialized; + const canCalculateFromCache = lifecycle.isInitialized() && reportDb.initialized && resultDb.initialized; if (!canCalculateFromCache) { return await storage.getServerDataInfo(); @@ -384,12 +272,14 @@ class Service { } public async getConfig() { - const cached = this.shouldUseServerCache() && configCache.initialized ? configCache.config : undefined; + if (lifecycle.isInitialized() && configCache.initialized) { + const cached = configCache.config; - if (cached) { - console.log(`[service] using cached config`); + if (cached) { + console.log(`[service] using cached config`); - return cached; + return cached; + } } const { result, error } = await storage.readConfigFile(); diff --git a/app/lib/service/lifecycle.ts b/app/lib/service/lifecycle.ts index 6fb0f41e..ae73abf2 100644 --- a/app/lib/service/lifecycle.ts +++ b/app/lib/service/lifecycle.ts @@ -1,7 +1,6 @@ import { configCache } from '@/app/lib/service/cache/config'; import { reportDb, resultDb } from '@/app/lib/service/db'; import { cronService } from '@/app/lib/service/cron'; -import { env } from '@/app/config/env'; import { isBuildStage } from '@/app/config/runtime'; const createdLifecycle = Symbol.for('playwright.reports.lifecycle'); @@ -29,10 +28,8 @@ export class Lifecycle { console.log('[lifecycle] Starting application initialization'); try { - if (env.USE_SERVER_CACHE) { - await Promise.all([configCache.init(), reportDb.init(), resultDb.init()]); - console.log('[lifecycle] Databases initialized successfully'); - } + await Promise.all([configCache.init(), reportDb.init(), resultDb.init()]); + console.log('[lifecycle] Databases initialized successfully'); if (!cronService.initialized && !isBuildStage) { await cronService.init(); diff --git a/app/lib/storage/fs.ts b/app/lib/storage/fs.ts index 4ea1c10f..516c1a2a 100644 --- a/app/lib/storage/fs.ts +++ b/app/lib/storage/fs.ts @@ -19,7 +19,6 @@ import { TMP_FOLDER, } from './constants'; import { processBatch } from './batch'; -import { handlePagination } from './pagination'; import { createDirectory } from './folders'; import { defaultConfig, isConfigValid, noConfigErr } from '@/app/lib/config'; @@ -33,10 +32,9 @@ import { type ServerDataInfo, type ResultDetails, ReadReportsOutput, - ReadReportsInput, - ReadResultsInput, ReportMetadata, ReportHistory, + ReportPath, } from '@/app/lib/storage'; import { SiteWhiteLabelConfig } from '@/app/types'; @@ -82,7 +80,7 @@ async function getResultsCount() { return zipFilesCount.length; } -export async function readResults(input?: ReadResultsInput) { +export async function readResults() { await createDirectoriesIfMissing(); const files = await fs.readdir(RESULTS_FOLDER); @@ -103,61 +101,26 @@ export async function readResults(input?: ReadResultsInput) { }, ); - const jsonFiles = stats.sort((a, b) => b.birthtimeMs - a.birthtimeMs); - - const fileContents: Result[] = await Promise.all( - jsonFiles.map(async (entry) => { - const content = await fs.readFile(entry.filePath, 'utf-8'); - - return { - size: entry.size, - sizeBytes: entry.sizeBytes, - ...JSON.parse(content), - }; - }), - ); - - let filteredResults = fileContents.filter((result) => (input?.project ? result.project === input.project : result)); - - // Filter by tags if provided - if (input?.tags && input.tags.length > 0) { - const notMetadataKeys = ['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project']; - - filteredResults = filteredResults.filter((result) => { - const resultTags = Object.entries(result) - .filter(([key]) => !notMetadataKeys.includes(key)) - .map(([key, value]) => `${key}: ${value}`); - - return input.tags!.some((selectedTag) => resultTags.includes(selectedTag)); - }); - } - - // Filter by search if provided - if (input?.search?.trim()) { - const searchTerm = input.search.toLowerCase().trim(); - - filteredResults = filteredResults.filter((result) => { - // Search in title, resultID, project, and all metadata fields - const searchableFields = [ - result.title, - result.resultID, - result.project, - ...Object.entries(result) - .filter(([key]) => !['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project'].includes(key)) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - - const paginatedResults = handlePagination(filteredResults, input?.pagination); + const results = await processBatch< + Stats & { + filePath: string; + size: string; + sizeBytes: number; + }, + Result + >({}, stats, 10, async (entry) => { + const content = await fs.readFile(entry.filePath, 'utf-8'); + + return { + size: entry.size, + sizeBytes: entry.sizeBytes, + ...JSON.parse(content), + }; + }); return { - results: paginatedResults.map((result) => ({ - ...result, - })), - total: filteredResults.length, + results, + total: results.length, }; } @@ -197,14 +160,13 @@ async function readOrParseReportMetadata(id: string, projectName: string): Promi return metadata; } -export async function readReports(input?: ReadReportsInput): Promise { +export async function readReports(): Promise { await createDirectoriesIfMissing(); const entries = await fs.readdir(REPORTS_FOLDER, { withFileTypes: true, recursive: true }); - const reportEntries = entries - .filter((entry) => !entry.isDirectory() && entry.name === 'index.html' && !(entry as any).path.endsWith('trace')) - .filter((entry) => (input?.ids ? input.ids.some((id) => (entry as any).path.includes(id)) : entry)) - .filter((entry) => (input?.project ? (entry as any).path.includes(input.project) : entry)); + const reportEntries = entries.filter( + (entry) => !entry.isDirectory() && entry.name === 'index.html' && !(entry as any).path.endsWith('trace'), + ); const stats = await processBatch( {}, @@ -217,21 +179,11 @@ export async function readReports(input?: ReadReportsInput): Promise b.birthtimeMs - a.birthtimeMs); - - const reportsWithProject = reportFiles - .map((file) => { - const id = path.basename(file.filePath); - const parentDir = path.basename(path.dirname(file.filePath)); - - const projectName = parentDir === REPORTS_PATH ? '' : parentDir; - - return Object.assign(file, { id, project: projectName }); - }) - .filter((report) => (input?.project ? input.project === report.project : report)); - - const allReports = await Promise.all( - reportsWithProject.map(async (file) => { + const reports = await processBatch( + {}, + stats, + 10, + async (file) => { const id = path.basename(file.filePath); const reportPath = path.dirname(file.filePath); const parentDir = path.basename(reportPath); @@ -250,37 +202,11 @@ export async function readReports(input?: ReadReportsInput): Promise { - // Search in title, reportID, project, and all metadata fields - const searchableFields = [ - report.title, - report.reportID, - report.project, - ...Object.entries(report) - .filter( - ([key]) => - !['reportID', 'title', 'createdAt', 'size', 'sizeBytes', 'project', 'reportUrl', 'stats'].includes(key), - ) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - - const paginatedReports = handlePagination(filteredReports, input?.pagination); - - return { reports: paginatedReports as ReportHistory[], total: filteredReports.length }; + return { reports: reports, total: reports.length }; } export async function deleteResults(resultsIds: string[]) { @@ -293,15 +219,12 @@ export async function deleteResult(resultId: string) { await Promise.allSettled([fs.unlink(`${resultPath}.json`), fs.unlink(`${resultPath}.zip`)]); } -export async function deleteReports(reportsIds: string[]) { - const { reports } = await readReports({ ids: reportsIds }); +export async function deleteReports(reports: ReportPath[]) { + const paths = reports.map((report) => (report.project ? `${report.project}/${report.reportID}` : report.reportID)); - const paths = reportsIds - .map((id) => reports.find((report) => report.reportID === id)) - .filter(Boolean) - .map((report) => (report?.project ? `${report.project}/${report.reportID}` : report?.reportID)); - - await Promise.allSettled(paths.map((path) => deleteReport(path!))); + await processBatch(undefined, paths, 10, async (path) => { + await deleteReport(path); + }); } export async function deleteReport(reportId: string) { diff --git a/app/lib/storage/pagination.ts b/app/lib/storage/pagination.ts index 80f58e25..24f65c14 100644 --- a/app/lib/storage/pagination.ts +++ b/app/lib/storage/pagination.ts @@ -3,20 +3,12 @@ export interface Pagination { offset: number; } -export const handlePagination = (items: T[], pagination?: Pagination): T[] => { - if (!pagination) { - return items; - } - - return items.slice(pagination.offset, pagination.offset + pagination.limit); -}; - export const parseFromRequest = (searchParams: URLSearchParams): Pagination => { const limitQuery = searchParams.get('limit') ?? ''; const offsetQuery = searchParams.get('offset') ?? ''; - const limit = limitQuery ? parseInt(limitQuery, 10) : 20; - const offset = offsetQuery ? parseInt(offsetQuery, 10) : 0; + const limit = limitQuery ? Number.parseInt(limitQuery, 10) : 20; + const offset = offsetQuery ? Number.parseInt(offsetQuery, 10) : 0; return { limit, offset }; }; diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index a9c783d6..f3f9723a 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -28,13 +28,12 @@ import { ResultDetails, ServerDataInfo, isReportHistory, - ReadReportsInput, ReadReportsOutput, - ReadResultsInput, ReadResultsOutput, ReportHistory, ReportMetadata, Storage, + ReportPath, } from './types'; import { bytesToString } from './format'; import { @@ -48,7 +47,6 @@ import { DATA_PATH, DATA_FOLDER, } from './constants'; -import { handlePagination } from './pagination'; import { getFileReportID } from './file'; import { parse } from '@/app/lib/parser'; @@ -58,7 +56,6 @@ import { withError } from '@/app/lib/withError'; import { env } from '@/app/config/env'; import { SiteWhiteLabelConfig } from '@/app/types'; import { defaultConfig, isConfigValid } from '@/app/lib/config'; -import { getTimestamp } from '@/app/lib/time'; const createClient = () => { const endPoint = env.S3_ENDPOINT; @@ -292,7 +289,7 @@ export class S3 implements Storage { return result!; } - async readResults(input?: ReadResultsInput): Promise { + async readResults(): Promise { await this.ensureBucketExist(); console.log('[s3] reading results'); @@ -330,7 +327,7 @@ export class S3 implements Storage { continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); - console.log(`[s3] found ${(jsonFiles ?? [])?.length} json files`); + console.log(`[s3] found ${jsonFiles.length} json files`); if (!jsonFiles) { return { @@ -339,21 +336,7 @@ export class S3 implements Storage { }; } - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - - jsonFiles.sort((a, b) => getTimestamp(b.LastModified) - getTimestamp(a.LastModified)); - - // check if we can apply pagination early - const noFilters = !input?.project && !input?.pagination; - - const resultFiles = noFilters ? handlePagination(jsonFiles, input?.pagination) : jsonFiles; - - const results = await processBatch<_Object, Result>(this, resultFiles, this.batchSize, async (file) => { + const results = await processBatch<_Object, Result>(this, jsonFiles, this.batchSize, async (file) => { console.log(`[s3.batch] reading result: ${JSON.stringify(file)}`); const response = await this.client.send( new GetObjectCommand({ @@ -374,44 +357,8 @@ export class S3 implements Storage { return parsed; }); - let filteredResults = results.filter((file) => (input?.project ? file.project === input.project : file)); - - // Filter by tags if provided - if (input?.tags && input.tags.length > 0) { - const notMetadataKeys = new Set(['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project']); - - filteredResults = filteredResults.filter((result) => { - const resultTags = Object.entries(result) - .filter(([key]) => !notMetadataKeys.has(key)) - .map(([key, value]) => `${key}: ${value}`); - - return input.tags!.some((selectedTag) => resultTags.includes(selectedTag)); - }); - } - - // Filter by search if provided - if (input?.search?.trim()) { - const searchTerm = input.search.toLowerCase().trim(); - - filteredResults = filteredResults.filter((result) => { - // Search in title, resultID, project, and all metadata fields - const searchableFields = [ - result.title, - result.resultID, - result.project, - ...Object.entries(result) - .filter(([key]) => !['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project'].includes(key)) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - - const currentFiles = noFilters ? results : handlePagination(filteredResults, input?.pagination); - return { - results: currentFiles.map((result) => { + results: results.map((result) => { const sizeBytes = resultSizes.get(result.resultID) ?? 0; return { @@ -420,11 +367,11 @@ export class S3 implements Storage { size: result.size ?? bytesToString(sizeBytes), }; }) as Result[], - total: noFilters ? jsonFiles.length : filteredResults.length, + total: results.length, }; } - async readReports(input?: ReadReportsInput): Promise { + async readReports(): Promise { await this.ensureBucketExist(); console.log(`[s3] reading reports from external storage`); @@ -464,12 +411,6 @@ export class S3 implements Storage { const projectName = parentDir === REPORTS_PATH ? '' : parentDir; - const noFilters = !input?.project && !input?.ids; - - const shouldFilterByProject = input?.project && projectName === input.project; - - const shouldFilterByID = input?.ids?.includes(id); - const report = { reportID: id, project: projectName, @@ -479,55 +420,16 @@ export class S3 implements Storage { sizeBytes: 0, }; - if (noFilters || shouldFilterByProject || shouldFilterByID) { - reports.push(report); - } + reports.push(report); } continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); - const getTimestamp = (date?: Date | string) => { - if (!date) return 0; - if (typeof date === 'string') return new Date(date).getTime(); - - return date.getTime(); - }; - - reports.sort((a, b) => getTimestamp(b.createdAt) - getTimestamp(a.createdAt)); - - const currentReports = handlePagination(reports, input?.pagination); - - const withMetadata = await this.getReportsMetadata(currentReports as ReportHistory[]); - - let filteredReports = withMetadata; - - // Filter by search if provided - if (input?.search && input.search.trim()) { - const searchTerm = input.search.toLowerCase().trim(); - - filteredReports = filteredReports.filter((report) => { - // Search in title, reportID, project, and all metadata fields - const searchableFields = [ - report.title, - report.reportID, - report.project, - ...Object.entries(report) - .filter( - ([key]) => - !['reportID', 'title', 'createdAt', 'size', 'sizeBytes', 'project', 'reportUrl', 'stats'].includes(key), - ) - .map(([key, value]) => `${key}: ${value}`), - ].filter(Boolean); - - return searchableFields.some((field) => field?.toLowerCase().includes(searchTerm)); - }); - } - - const finalReports = handlePagination(filteredReports, input?.pagination); + const withMetadata = await this.getReportsMetadata(reports as ReportHistory[]); return { - reports: finalReports.map((report) => { + reports: withMetadata.map((report) => { const sizeBytes = reportSizes.get(report.reportID) ?? 0; return { @@ -536,7 +438,7 @@ export class S3 implements Storage { size: bytesToString(sizeBytes), }; }), - total: filteredReports.length, + total: withMetadata.length, }; } @@ -645,8 +547,9 @@ export class S3 implements Storage { return files; } - async deleteReports(reportIDs: string[]): Promise { - const objects = await this.getReportObjects(reportIDs); + async deleteReports(reports: ReportPath[]): Promise { + const ids = reports.map((r) => r.reportID); + const objects = await this.getReportObjects(ids); await withError(this.clear(...objects)); } @@ -722,7 +625,9 @@ export class S3 implements Storage { console.log( `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, ); - console.log(`[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`); + console.log( + `[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`, + ); stream.pause(); diff --git a/app/lib/storage/types.ts b/app/lib/storage/types.ts index b0e8079a..69b56e77 100644 --- a/app/lib/storage/types.ts +++ b/app/lib/storage/types.ts @@ -8,10 +8,10 @@ import { type ReportInfo, type ReportTest } from '@/app/lib/parser/types'; export interface Storage { getServerDataInfo: () => Promise; readFile: (targetPath: string, contentType: string | null) => Promise; - readResults: (input?: ReadResultsInput) => Promise; - readReports: (input?: ReadReportsInput) => Promise; + readResults: () => Promise; + readReports: () => Promise; deleteResults: (resultIDs: string[]) => Promise; - deleteReports: (reportIDs: string[]) => Promise; + deleteReports: (reports: ReportPath[]) => Promise; saveResult: (filename: string, stream: PassThrough) => Promise; saveResultDetails: (resultID: string, resultDetails: ResultDetails, size: number) => Promise; generateReport: (resultsIds: string[], metadata?: ReportMetadata) => Promise; @@ -21,6 +21,11 @@ export interface Storage { ) => Promise<{ result: SiteWhiteLabelConfig; error: Error | null }>; } +export interface ReportPath { + reportID: string; + project: string; +} + export interface ReadResultsInput { pagination?: Pagination; project?: string; diff --git a/readme.md b/readme.md index 30bdc58d..94d60e84 100644 --- a/readme.md +++ b/readme.md @@ -84,18 +84,17 @@ The Playwright Reports Server provides APIs for managing and generating reports The app is configured with environment variables, so it could be specified as `.env` file as well, however there are no mandatory options. -| Name | Description | Default | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------- | -| `API_TOKEN` | API token for [Authorization](#authorization) | | -| `AUTH_SECRET` | Secret to encrypt JWT | | -| `UI_AUTH_EXPIRE_HOURS` | Duration of auth session | `"2"` | -| `USE_SERVER_CACHE` | Use sqlite3 for storing metadata for results and reports, config caching - improves UX, reduces impact on a s3/fs | `false` | -| `SERVER_CACHE_REFRESH_CRON` | | | -| `DATA_STORAGE` | Where to store data, check for additional configuration [Storage Options](#storage-options) | `"fs"` | -| `JIRA_BASE_URL` | Jira instance URL (e.g., https://your-domain.atlassian.net) | | -| `JIRA_EMAIL` | Jira account email address | | -| `JIRA_API_TOKEN` | Jira API token for authentication | | -| `JIRA_PROJECT_KEY` | Default Jira project key for ticket creation | | +| Name | Description | Default | +| --------------------------- | ------------------------------------------------------------------------------------------- | ------- | +| `API_TOKEN` | API token for [Authorization](#authorization) | | +| `AUTH_SECRET` | Secret to encrypt JWT | | +| `UI_AUTH_EXPIRE_HOURS` | Duration of auth session | `"2"` | +| `SERVER_CACHE_REFRESH_CRON` | | | +| `DATA_STORAGE` | Where to store data, check for additional configuration [Storage Options](#storage-options) | `"fs"` | +| `JIRA_BASE_URL` | Jira instance URL (e.g., https://your-domain.atlassian.net) | | +| `JIRA_EMAIL` | Jira account email address | | +| `JIRA_API_TOKEN` | Jira API token for authentication | | +| `JIRA_PROJECT_KEY` | Default Jira project key for ticket creation | | ## API Routes From d2857d2aea0b5eba73b9d87c89e4d61f8b2d4613 Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Sun, 23 Nov 2025 01:15:43 +0200 Subject: [PATCH 19/26] fix: docker healthcheck takes too much time --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 009363db..8af36465 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,4 +72,5 @@ ENV PORT=3000 # https://nextjs.org/docs/pages/api-reference/next-config-js/output CMD ["sh", "-c", "HOSTNAME=0.0.0.0 node server.js"] -HEALTHCHECK --interval=3m --timeout=3s CMD curl -f http://localhost:$PORT/api/ping || exit 1 +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:$PORT/api/ping || exit 1 From da40c0d648dec0b451c652747cf5ca30f377e7aa Mon Sep 17 00:00:00 2001 From: Shelex <11396724+Shelex@users.noreply.github.com> Date: Fri, 5 Dec 2025 05:42:58 +0200 Subject: [PATCH 20/26] feat: refactor from nextjs to fastify + react --- .eslintignore | 20 - .eslintrc.json | 83 - .gitignore | 7 +- .prettierignore | 6 - .prettierrc | 9 - Dockerfile | 80 +- Makefile | 98 + app/layout.tsx | 153 +- app/lib/service/index.ts | 637 +- app/lib/storage/fs.ts | 704 +- app/lib/storage/s3.ts | 2437 ++--- app/lib/storage/types.ts | 138 +- app/settings/components/DatabaseInfo.tsx | 48 - backend/.env.example | 24 + backend/package-lock.json | 6439 +++++++++++++ backend/package.json | 48 + backend/public/favicon.ico | Bin 0 -> 262206 bytes backend/public/logo.svg | 10 + backend/src/config/env.ts | 49 + backend/src/config/site.ts | 63 + backend/src/index.ts | 106 + backend/src/lib/auth.ts | 7 + backend/src/lib/config.ts | 37 + backend/src/lib/constants.ts | 3 + backend/src/lib/network.ts | 19 + backend/src/lib/parser/index.ts | 64 + backend/src/lib/parser/types.ts | 72 + backend/src/lib/pw.ts | 131 + backend/src/lib/query-cache.ts | 27 + backend/src/lib/schemas/index.ts | 162 + backend/src/lib/service/cache/config.ts | 56 + backend/src/lib/service/cron.ts | 176 + backend/src/lib/service/db/db.ts | 176 + backend/src/lib/service/db/forceInit.ts | 23 + backend/src/lib/service/db/index.ts | 4 + backend/src/lib/service/db/reports.sqlite.ts | 385 + backend/src/lib/service/db/results.sqlite.ts | 346 + backend/src/lib/service/index.ts | 422 + backend/src/lib/service/jira.ts | 310 + backend/src/lib/service/lifecycle.ts | 70 + backend/src/lib/storage/batch.ts | 18 + backend/src/lib/storage/constants.ts | 23 + backend/src/lib/storage/file.ts | 17 + backend/src/lib/storage/folders.ts | 10 + backend/src/lib/storage/format.ts | 18 + backend/src/lib/storage/fs.ts | 493 + backend/src/lib/storage/index.ts | 7 + backend/src/lib/storage/pagination.ts | 14 + backend/src/lib/storage/s3.ts | 1359 +++ backend/src/lib/storage/types.ts | 121 + backend/src/lib/tailwind.ts | 52 + backend/src/lib/time.ts | 27 + backend/src/lib/transformers.ts | 68 + backend/src/lib/url.ts | 8 + backend/src/lib/validation/index.ts | 85 + backend/src/lib/withError.ts | 14 + backend/src/routes/auth.ts | 239 + backend/src/routes/config.ts | 276 + backend/src/routes/index.ts | 16 + backend/src/routes/jira.ts | 173 + backend/src/routes/reports.ts | 186 + backend/src/routes/results.ts | 328 + backend/src/routes/serve.ts | 83 + backend/src/types/index.ts | 45 + backend/tsconfig.json | 24 + backend/tsconfig.node.json | 10 + backend/vite.config.ts | 10 + frontend/index.html | 21 + frontend/package-lock.json | 8151 +++++++++++++++++ frontend/package.json | 46 + frontend/postcss.config.cjs | 6 + frontend/src/App.tsx | 32 + frontend/src/components/Layout.tsx | 35 + frontend/src/components/aside.tsx | 81 + frontend/src/components/date-format.tsx | 16 + .../src/components/delete-report-button.tsx | 104 + .../src/components/delete-results-button.tsx | 113 + .../src/components/generate-report-button.tsx | 217 + frontend/src/components/header-links.tsx | 50 + frontend/src/components/icons.tsx | 436 + .../src/components/inline-stats-circle.tsx | 35 + frontend/src/components/jira-ticket-modal.tsx | 349 + frontend/src/components/login-form.tsx | 115 + frontend/src/components/navbar.tsx | 95 + frontend/src/components/page-layout.tsx | 81 + frontend/src/components/primitives.ts | 53 + frontend/src/components/project-select.tsx | 62 + .../components/report-details/file-list.tsx | 92 + .../components/report-details/primitives.ts | 31 + .../report-details/report-stats.tsx | 36 + .../components/report-details/suite-tree.tsx | 173 + .../components/report-details/test-info.tsx | 128 + .../report-details/tests-filters.tsx | 106 + frontend/src/components/report-trends.tsx | 65 + frontend/src/components/reports-table.tsx | 290 + frontend/src/components/reports.tsx | 18 + frontend/src/components/results-table.tsx | 243 + frontend/src/components/results.tsx | 60 + .../settings/components/AddLinkModal.tsx | 78 + .../settings/components/CronConfiguration.tsx | 218 + .../settings/components/DatabaseInfo.tsx | 50 + .../settings/components/EnvironmentInfo.tsx | 62 + .../settings/components/JiraConfiguration.tsx | 251 + .../components/ServerConfiguration.tsx | 443 + frontend/src/components/settings/types.ts | 31 + frontend/src/components/stat-chart.tsx | 113 + .../components/table-pagination-options.tsx | 77 + frontend/src/components/tag-select.tsx | 59 + frontend/src/components/theme-switch.tsx | 80 + frontend/src/components/trend-chart.tsx | 172 + frontend/src/components/ui/chart.tsx | 388 + .../src/components/upload-results-button.tsx | 308 + frontend/src/config/fonts.ts | 11 + frontend/src/config/network.ts | 15 + frontend/src/config/site.ts | 67 + frontend/src/config/url.ts | 6 + frontend/src/hooks/useAuth.ts | 48 + frontend/src/hooks/useAuthConfig.ts | 29 + frontend/src/hooks/useMutation.ts | 60 + frontend/src/hooks/useQuery.ts | 87 + frontend/src/lib/auth.ts | 68 + frontend/src/lib/constants.ts | 1 + frontend/src/lib/network.ts | 24 + frontend/src/lib/query-cache.ts | 33 + frontend/src/lib/storage.ts | 19 + frontend/src/lib/tailwind.ts | 46 + frontend/src/lib/time.ts | 23 + frontend/src/lib/transformers.ts | 37 + frontend/src/lib/url.ts | 78 + frontend/src/main.tsx | 13 + frontend/src/pages/HomePage.tsx | 5 + frontend/src/pages/LoginPage.tsx | 10 + frontend/src/pages/ReportDetailPage.tsx | 61 + frontend/src/pages/ReportsPage.tsx | 8 + frontend/src/pages/ResultsPage.tsx | 8 + frontend/src/pages/SettingsPage.tsx | 265 + frontend/src/pages/TrendsPage.tsx | 5 + frontend/src/providers/index.tsx | 43 + frontend/src/styles/globals.css | 30 + frontend/src/styles/globals.ts | 46 + frontend/src/types/index.ts | 158 + frontend/src/types/parser.ts | 58 + frontend/src/types/storage.ts | 82 + frontend/src/types/types.ts | 1 + frontend/src/vite-env.d.ts | 11 + frontend/tailwind.config.ts | 43 + frontend/tsconfig.json | 24 + frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 46 + package-lock.json | 6236 +++---------- package.json | 24 +- pages/api/result/upload.ts | 285 - playwright.config.ts | 11 +- prettier.config.js | 6 - scripts/cleanup-dev.sh | 140 + scripts/test-server-start.sh | 212 + tests/api/controllers/result.controller.ts | 90 +- tests/api/generate.test.ts | 132 +- tests/api/setup.ts | 479 + 159 files changed, 32660 insertions(+), 7577 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 Makefile delete mode 100644 app/settings/components/DatabaseInfo.tsx create mode 100644 backend/.env.example create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/public/favicon.ico create mode 100644 backend/public/logo.svg create mode 100644 backend/src/config/env.ts create mode 100644 backend/src/config/site.ts create mode 100644 backend/src/index.ts create mode 100644 backend/src/lib/auth.ts create mode 100644 backend/src/lib/config.ts create mode 100644 backend/src/lib/constants.ts create mode 100644 backend/src/lib/network.ts create mode 100644 backend/src/lib/parser/index.ts create mode 100644 backend/src/lib/parser/types.ts create mode 100644 backend/src/lib/pw.ts create mode 100644 backend/src/lib/query-cache.ts create mode 100644 backend/src/lib/schemas/index.ts create mode 100644 backend/src/lib/service/cache/config.ts create mode 100644 backend/src/lib/service/cron.ts create mode 100644 backend/src/lib/service/db/db.ts create mode 100644 backend/src/lib/service/db/forceInit.ts create mode 100644 backend/src/lib/service/db/index.ts create mode 100644 backend/src/lib/service/db/reports.sqlite.ts create mode 100644 backend/src/lib/service/db/results.sqlite.ts create mode 100644 backend/src/lib/service/index.ts create mode 100644 backend/src/lib/service/jira.ts create mode 100644 backend/src/lib/service/lifecycle.ts create mode 100644 backend/src/lib/storage/batch.ts create mode 100644 backend/src/lib/storage/constants.ts create mode 100644 backend/src/lib/storage/file.ts create mode 100644 backend/src/lib/storage/folders.ts create mode 100644 backend/src/lib/storage/format.ts create mode 100644 backend/src/lib/storage/fs.ts create mode 100644 backend/src/lib/storage/index.ts create mode 100644 backend/src/lib/storage/pagination.ts create mode 100644 backend/src/lib/storage/s3.ts create mode 100644 backend/src/lib/storage/types.ts create mode 100644 backend/src/lib/tailwind.ts create mode 100644 backend/src/lib/time.ts create mode 100644 backend/src/lib/transformers.ts create mode 100644 backend/src/lib/url.ts create mode 100644 backend/src/lib/validation/index.ts create mode 100644 backend/src/lib/withError.ts create mode 100644 backend/src/routes/auth.ts create mode 100644 backend/src/routes/config.ts create mode 100644 backend/src/routes/index.ts create mode 100644 backend/src/routes/jira.ts create mode 100644 backend/src/routes/reports.ts create mode 100644 backend/src/routes/results.ts create mode 100644 backend/src/routes/serve.ts create mode 100644 backend/src/types/index.ts create mode 100644 backend/tsconfig.json create mode 100644 backend/tsconfig.node.json create mode 100644 backend/vite.config.ts create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.cjs create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/aside.tsx create mode 100644 frontend/src/components/date-format.tsx create mode 100644 frontend/src/components/delete-report-button.tsx create mode 100644 frontend/src/components/delete-results-button.tsx create mode 100644 frontend/src/components/generate-report-button.tsx create mode 100644 frontend/src/components/header-links.tsx create mode 100644 frontend/src/components/icons.tsx create mode 100644 frontend/src/components/inline-stats-circle.tsx create mode 100644 frontend/src/components/jira-ticket-modal.tsx create mode 100644 frontend/src/components/login-form.tsx create mode 100644 frontend/src/components/navbar.tsx create mode 100644 frontend/src/components/page-layout.tsx create mode 100644 frontend/src/components/primitives.ts create mode 100644 frontend/src/components/project-select.tsx create mode 100644 frontend/src/components/report-details/file-list.tsx create mode 100644 frontend/src/components/report-details/primitives.ts create mode 100644 frontend/src/components/report-details/report-stats.tsx create mode 100644 frontend/src/components/report-details/suite-tree.tsx create mode 100644 frontend/src/components/report-details/test-info.tsx create mode 100644 frontend/src/components/report-details/tests-filters.tsx create mode 100644 frontend/src/components/report-trends.tsx create mode 100644 frontend/src/components/reports-table.tsx create mode 100644 frontend/src/components/reports.tsx create mode 100644 frontend/src/components/results-table.tsx create mode 100644 frontend/src/components/results.tsx create mode 100644 frontend/src/components/settings/components/AddLinkModal.tsx create mode 100644 frontend/src/components/settings/components/CronConfiguration.tsx create mode 100644 frontend/src/components/settings/components/DatabaseInfo.tsx create mode 100644 frontend/src/components/settings/components/EnvironmentInfo.tsx create mode 100644 frontend/src/components/settings/components/JiraConfiguration.tsx create mode 100644 frontend/src/components/settings/components/ServerConfiguration.tsx create mode 100644 frontend/src/components/settings/types.ts create mode 100644 frontend/src/components/stat-chart.tsx create mode 100644 frontend/src/components/table-pagination-options.tsx create mode 100644 frontend/src/components/tag-select.tsx create mode 100644 frontend/src/components/theme-switch.tsx create mode 100644 frontend/src/components/trend-chart.tsx create mode 100644 frontend/src/components/ui/chart.tsx create mode 100644 frontend/src/components/upload-results-button.tsx create mode 100644 frontend/src/config/fonts.ts create mode 100644 frontend/src/config/network.ts create mode 100644 frontend/src/config/site.ts create mode 100644 frontend/src/config/url.ts create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/hooks/useAuthConfig.ts create mode 100644 frontend/src/hooks/useMutation.ts create mode 100644 frontend/src/hooks/useQuery.ts create mode 100644 frontend/src/lib/auth.ts create mode 100644 frontend/src/lib/constants.ts create mode 100644 frontend/src/lib/network.ts create mode 100644 frontend/src/lib/query-cache.ts create mode 100644 frontend/src/lib/storage.ts create mode 100644 frontend/src/lib/tailwind.ts create mode 100644 frontend/src/lib/time.ts create mode 100644 frontend/src/lib/transformers.ts create mode 100644 frontend/src/lib/url.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/HomePage.tsx create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/pages/ReportDetailPage.tsx create mode 100644 frontend/src/pages/ReportsPage.tsx create mode 100644 frontend/src/pages/ResultsPage.tsx create mode 100644 frontend/src/pages/SettingsPage.tsx create mode 100644 frontend/src/pages/TrendsPage.tsx create mode 100644 frontend/src/providers/index.tsx create mode 100644 frontend/src/styles/globals.css create mode 100644 frontend/src/styles/globals.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/types/parser.ts create mode 100644 frontend/src/types/storage.ts create mode 100644 frontend/src/types/types.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts delete mode 100644 pages/api/result/upload.ts delete mode 100644 prettier.config.js create mode 100755 scripts/cleanup-dev.sh create mode 100755 scripts/test-server-start.sh create mode 100644 tests/api/setup.ts diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index af6ab76f..00000000 --- a/.eslintignore +++ /dev/null @@ -1,20 +0,0 @@ -.now/* -*.css -.changeset -dist -esm/* -public/* -tests/* -scripts/* -*.config.js -.DS_Store -node_modules -coverage -.next -build -!.commitlintrc.cjs -!.lintstagedrc.cjs -!jest.config.js -!plopfile.js -!react-shim.js -!tsup.config.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index d2fbabe5..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/eslintrc.json", - "env": { - "browser": false, - "es2021": true, - "node": true - }, - "extends": [ - "plugin:react/recommended", - "plugin:prettier/recommended", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended" - ], - "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 12, - "sourceType": "module" - }, - "settings": { - "react": { - "version": "detect" - } - }, - "rules": { - "no-console": "off", - "react/prop-types": "off", - "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off", - "react-hooks/exhaustive-deps": "off", - "jsx-a11y/click-events-have-key-events": "warn", - "jsx-a11y/interactive-supports-focus": "warn", - "prettier/prettier": "warn", - "no-unused-vars": "off", - "unused-imports/no-unused-vars": "off", - "unused-imports/no-unused-imports": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "args": "after-used", - "ignoreRestSiblings": false, - "argsIgnorePattern": "^_.*?$" - } - ], - "import/order": [ - "warn", - { - "groups": ["type", "builtin", "object", "external", "internal", "parent", "sibling", "index"], - "pathGroups": [ - { - "pattern": "~/**", - "group": "external", - "position": "after" - } - ], - "newlines-between": "always" - } - ], - "react/self-closing-comp": "warn", - "react/jsx-sort-props": [ - "warn", - { - "callbacksLast": true, - "shorthandFirst": true, - "noSortAlphabetically": false, - "reservedFirst": true - } - ], - "padding-line-between-statements": [ - "warn", - { "blankLine": "always", "prev": "*", "next": "return" }, - { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, - { - "blankLine": "any", - "prev": ["const", "let", "var"], - "next": ["const", "let", "var"] - } - ] - } -} diff --git a/.gitignore b/.gitignore index 93df80f9..13de00a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # data -/data -.tmp +**/data +**/.tmp # dependencies /node_modules @@ -18,6 +18,7 @@ # production /build +/backend/dist # misc .DS_Store @@ -38,8 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -data/reports -data/results # vscode .vscode diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 746669b4..00000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -.npm/ -node_modules/ -.eslintignore -.prettierignore -package.json -package-lock.json diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 957a15d6..00000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "semi": true, - "trailingComma": "all", - "singleQuote": true, - "printWidth": 120, - "tabWidth": 2, - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/Dockerfile b/Dockerfile index 8af36465..eceec05c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,33 @@ FROM node:22-alpine AS base -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +# Install dependencies for backend +FROM base AS backend-deps RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Install dependencies based on the preferred package manager -COPY package.json package-lock.json* ./ +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json* ./ RUN npm ci -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -ARG API_BASE_PATH="" -ENV API_BASE_PATH=$API_BASE_PATH - -ARG ASSETS_BASE_PATH="" -ENV ASSETS_BASE_PATH=$ASSETS_BASE_PATH +# Install dependencies for frontend +FROM base AS frontend-deps +WORKDIR /app/frontend +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm ci -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -ENV NEXT_TELEMETRY_DISABLED=1 +# Build frontend +FROM base AS frontend-builder +WORKDIR /app/frontend +COPY --from=frontend-deps /app/frontend/node_modules ./node_modules +COPY frontend/ . +RUN npm run build +# Build backend +FROM base AS backend-builder +WORKDIR /app/backend +COPY --from=backend-deps /app/backend/node_modules ./node_modules +COPY backend/ . RUN npm run build -# Production image, copy all the files and run next +# Production image FROM base AS runner WORKDIR /app @@ -37,22 +35,19 @@ ENV NODE_ENV=production RUN apk add --no-cache curl -# Uncomment the following line in case you want to disable telemetry during runtime. -# ENV NEXT_TELEMETRY_DISABLED 1 - RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 --ingroup nodejs nextjs + adduser --system --uid 1001 --ingroup nodejs appuser -COPY --from=builder --chown=nextjs:nodejs /app/public ./public +# Copy backend build +COPY --from=backend-builder --chown=appuser:nodejs /app/backend/dist ./backend/dist +COPY --from=backend-builder --chown=appuser:nodejs /app/backend/node_modules ./backend/node_modules +COPY --from=backend-builder --chown=appuser:nodejs /app/backend/package.json ./backend/package.json -# Set the correct permission for prerender cache -RUN mkdir .next && \ - chown nextjs:nodejs .next +# Copy frontend build +COPY --from=frontend-builder --chown=appuser:nodejs /app/frontend/dist ./frontend/dist -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Copy public assets +COPY --from=frontend-builder --chown=appuser:nodejs /app/frontend/public ./frontend/public # Create folders required for storing results and reports ARG DATA_DIR=/app/data @@ -60,17 +55,18 @@ ARG RESULTS_DIR=${DATA_DIR}/results ARG REPORTS_DIR=${DATA_DIR}/reports ARG TEMP_DIR=/app/.tmp RUN mkdir -p ${DATA_DIR} ${RESULTS_DIR} ${REPORTS_DIR} ${TEMP_DIR} && \ - chown -R nextjs:nodejs ${DATA_DIR} ${TEMP_DIR} + chown -R appuser:nodejs ${DATA_DIR} ${TEMP_DIR} + +USER appuser -USER nextjs +EXPOSE 3001 -EXPOSE 3000 +ENV PORT=3001 +ENV FRONTEND_DIST=/app/frontend/dist -ENV PORT=3000 +WORKDIR /app/backend -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/next-config-js/output -CMD ["sh", "-c", "HOSTNAME=0.0.0.0 node server.js"] +CMD ["node", "dist/index.js"] HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD curl -f http://localhost:$PORT/api/ping || exit 1 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7437b4ee --- /dev/null +++ b/Makefile @@ -0,0 +1,98 @@ +.PHONY: help install dev build start clean test format lint backend-install frontend-install backend-dev frontend-dev backend-build frontend-build cleanup + +help: + @echo "Playwright Reports Server - Simplified Makefile commands:" + @echo "" + @echo "Main commands:" + @echo " make install - Install dependencies for both backend and frontend" + @echo " make dev - Start both backend and frontend in development mode" + @echo " make build - Build both backend and frontend for production" + @echo " make start - Start production server (backend + frontend)" + @echo " make clean - Clean build artifacts and dependencies" + @echo " make cleanup - Kill all development processes and clean up" + @echo " make test - Run tests" + @echo " make typecheck - Run TypeScript type checking" + @echo " make format - Format code using Biome" + @echo " make lint - Check and fix code using Biome" + @echo "" + @echo "Individual services:" + @echo " make backend-dev - Start backend only in development mode" + @echo " make frontend-dev - Start frontend only in development mode" + @echo "" + @echo "Equivalent npm commands (simpler):" + @echo " npm install - Install dependencies" + @echo " npm run dev - Start both backend and frontend (tsx + concurrently)" + @echo " npm run dev:backend - Start backend only (tsx watch)" + @echo " npm run dev:frontend - Start frontend only (Vite HMR)" + @echo " npm run build - Build both for production" + @echo " npm run start - Start production server" + @echo " npm run clean - Clean up development environment" + +# Install all dependencies +install: + @echo "Installing backend dependencies..." + cd backend && npm install + @echo "Installing frontend dependencies..." + cd frontend && npm install + @echo "โœ“ All dependencies installed" + +# Development mode - uses npm run dev (tsx + concurrently) +dev: + @echo "Starting development mode with tsx + concurrently..." + @echo "Backend will run on http://localhost:3001 (tsx watch)" + @echo "Frontend will run on http://localhost:3000 (Vite HMR)" + @echo "Press Ctrl+C to stop both services" + npx concurrently "npm run dev:backend" "npm run dev:frontend" + +backend-dev: + @echo "Starting backend in development mode with tsx..." + @echo "Backend will run on http://localhost:3001" + @echo "Press Ctrl+C to stop" + cd backend && npm run dev:backend + +frontend-dev: + @echo "Starting frontend in development mode with Vite..." + @echo "Frontend will run on http://localhost:3000" + @echo "Press Ctrl+C to stop" + cd frontend && npm run dev:frontend + +build: + @echo "Building backend and frontend for production..." + npm run build + +start: + @echo "Starting production server..." + @echo "Server will run on http://localhost:3001" + @echo "Fastify will serve both API and frontend static files" + npm run start + +clean: + @echo "Cleaning build artifacts..." + rm -rf backend/dist backend/node_modules + rm -rf frontend/dist frontend/node_modules + @echo "โœ“ Clean complete" + +test: + @echo "Running tests..." + cd backend && npm test || true + @echo "โœ“ Tests complete" + +typecheck: + @echo "Running type checks..." + cd backend && npm run typecheck + cd frontend && npm run typecheck + @echo "โœ“ Type checking complete" + +format: + @echo "Formatting code with Biome..." + npx biome format --write ./backend/src ./frontend/src + @echo "โœ“ Code formatting complete" + +lint: + @echo "Checking and fixing code with Biome..." + npx biome check ./backend/src ./frontend/src --write + @echo "โœ“ Code linting complete" + +cleanup: + @echo "Cleaning up development environment..." + @./scripts/cleanup-dev.sh diff --git a/app/layout.tsx b/app/layout.tsx index d47239a8..21e7c360 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,79 +1,92 @@ -import '@/app/styles/globals.css'; -import { Metadata, Viewport } from 'next'; -import Link from 'next/link'; -import clsx from 'clsx'; -import { Toaster } from 'sonner'; - -import { Providers } from './providers'; - -import { siteConfig } from '@/app/config/site'; -import { fontSans } from '@/app/config/fonts'; -import { Navbar } from '@/app/components/navbar'; -import { Aside } from '@/app/components/aside'; -import { service } from '@/app/lib/service'; -import { withBase } from '@/app/lib/url'; +import "@/app/styles/globals.css"; +import clsx from "clsx"; +import type { Metadata, Viewport } from "next"; +import Link from "next/link"; +import { Toaster } from "sonner"; +import { Aside } from "@/app/components/aside"; +import { Navbar } from "@/app/components/navbar"; +import { fontSans } from "@/app/config/fonts"; +import { siteConfig } from "@/app/config/site"; +import { service } from "@/app/lib/service"; +import { withBase } from "@/app/lib/url"; +import { Providers } from "./providers"; export async function generateMetadata(): Promise { - const config = await service.getConfig(); + const config = await service.getConfig(); - return { - title: { - default: siteConfig.name, - template: `%s - ${siteConfig.name}`, - }, - description: siteConfig.description, - icons: { - icon: withBase(config?.faviconPath ? `/api/static${config.faviconPath}` : '/favicon.ico'), - }, - }; + return { + title: { + default: siteConfig.name, + template: `%s - ${siteConfig.name}`, + }, + description: siteConfig.description, + icons: { + icon: withBase( + config?.faviconPath + ? `/api/static${config.faviconPath}` + : "/favicon.ico", + ), + }, + }; } export async function generateViewport(): Promise { - return { - themeColor: [ - { media: '(prefers-color-scheme: light)', color: 'white' }, - { media: '(prefers-color-scheme: dark)', color: 'black' }, - ], - }; + return { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "black" }, + ], + }; } -export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { - return ( - - - - - - - - - -
- -
-
-
- - Powered by -

CyborgTests

- -
-
-
- - - ); +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + + + + + + + +
+ +
+
+
+ + Powered by +

CyborgTests

+ +
+
+
+ + + ); } diff --git a/app/lib/service/index.ts b/app/lib/service/index.ts index f98b2f6c..bd38e954 100644 --- a/app/lib/service/index.ts +++ b/app/lib/service/index.ts @@ -1,306 +1,381 @@ -import { PassThrough, Readable } from 'node:stream'; - -import { withError } from '../withError'; -import { bytesToString, getUniqueProjectsList } from '../storage/format'; -import { serveReportRoute } from '../constants'; -import { DEFAULT_STREAM_CHUNK_SIZE } from '../storage/constants'; - -import { lifecycle } from '@/app/lib/service/lifecycle'; -import { configCache } from '@/app/lib/service/cache/config'; -import { reportDb, resultDb } from '@/app/lib/service/db'; +import { type PassThrough, Readable } from "node:stream"; +import { env } from "@/app/config/env"; +import { defaultConfig } from "@/app/lib/config"; +import { isValidPlaywrightVersion } from "@/app/lib/pw"; +import { configCache } from "@/app/lib/service/cache/config"; +import { reportDb, resultDb } from "@/app/lib/service/db"; +import { lifecycle } from "@/app/lib/service/lifecycle"; import { - type ReadReportsInput, - ReadResultsInput, - ReadResultsOutput, - ReportMetadata, - ReportPath, - ResultDetails, - ServerDataInfo, - storage, -} from '@/app/lib/storage'; -import { SiteWhiteLabelConfig } from '@/app/types'; -import { defaultConfig } from '@/app/lib/config'; -import { env } from '@/app/config/env'; -import { type S3 } from '@/app/lib/storage/s3'; -import { isValidPlaywrightVersion } from '@/app/lib/pw'; - -const runningService = Symbol.for('playwright.reports.service'); -const instance = globalThis as typeof globalThis & { [runningService]?: Service }; + type ReadReportsInput, + type ReadResultsInput, + type ReadResultsOutput, + type ReportMetadata, + type ReportPath, + type ResultDetails, + type ServerDataInfo, + storage, +} from "@/app/lib/storage"; +import type { S3 } from "@/app/lib/storage/s3"; +import type { SiteWhiteLabelConfig } from "@/app/types"; +import { serveReportRoute } from "../constants"; +import { DEFAULT_STREAM_CHUNK_SIZE } from "../storage/constants"; +import { bytesToString, getUniqueProjectsList } from "../storage/format"; +import { withError } from "../withError"; + +const runningService = Symbol.for("playwright.reports.service"); +const instance = globalThis as typeof globalThis & { + [runningService]?: Service; +}; class Service { - public static getInstance() { - console.log(`[service] get instance`); - instance[runningService] ??= new Service(); - - return instance[runningService]; - } - - public async getReports(input?: ReadReportsInput) { - console.log(`[service] getReports`); - - return reportDb.query(input); - } - - public async getReport(id: string) { - console.log(`[service] getReport ${id}`); - - const report = reportDb.getByID(id); - - return report!; - } - - private async findLatestPlaywrightVersionFromResults(resultIds: string[]) { - for (const resultId of resultIds) { - const { result: results, error } = await withError(this.getResults({ search: resultId })); - - if (error || !results) { - continue; - } - - const [latestResult] = results.results; - - if (!latestResult) { - continue; - } - - const latestVersion = latestResult?.playwrightVersion; - - if (latestVersion) { - return latestVersion; - } - } - } - - private async findLatestPlaywrightVersion(resultIds: string[]) { - const versionFromResults = await this.findLatestPlaywrightVersionFromResults(resultIds); - - if (versionFromResults) { - return versionFromResults; - } - - // just in case version not found in results, we can try to get it from latest reports - const { result: reportsArray, error } = await withError(this.getReports({ pagination: { limit: 10, offset: 0 } })); - - if (error || !reportsArray) { - return ''; - } - - const reportWithVersion = reportsArray.reports.find((report) => !!report.metadata?.playwrightVersion); - - if (!reportWithVersion) { - return ''; - } - - return reportWithVersion.metadata.playwrightVersion; - } - - public async generateReport( - resultsIds: string[], - metadata?: ReportMetadata, - ): Promise<{ reportId: string; reportUrl: string; metadata: ReportMetadata }> { - const version = isValidPlaywrightVersion(metadata?.playwrightVersion) - ? metadata?.playwrightVersion - : await this.findLatestPlaywrightVersion(resultsIds); - - const metadataWithVersion = { ...(metadata ?? {}), playwrightVersion: version ?? '' }; - - const reportId = await storage.generateReport(resultsIds, metadataWithVersion); - - const report = await this.getReport(reportId); - - reportDb.onCreated(report); - - const projectPath = metadata?.project ? `${encodeURI(metadata.project)}/` : ''; - const reportUrl = `${serveReportRoute}/${projectPath}${reportId}/index.html`; - - return { reportId, reportUrl, metadata: metadataWithVersion }; - } - - public async deleteReports(reportIDs: string[]) { - const entries: ReportPath[] = []; - - for (const id of reportIDs) { - const report = await this.getReport(id); - - entries.push({ reportID: id, project: report.project }); - } - - const { error } = await withError(storage.deleteReports(entries)); - - if (error) { - throw error; - } - - reportDb.onDeleted(reportIDs); - } - - public async getReportsProjects(): Promise { - const { reports } = await this.getReports(); - const projects = getUniqueProjectsList(reports); - - return projects; - } - - public async getResults(input?: ReadResultsInput): Promise { - console.log(`[results service] getResults`); - - return resultDb.query(input); - } - - public async deleteResults(resultIDs: string[]): Promise { - const { error } = await withError(storage.deleteResults(resultIDs)); - - if (error) { - throw error; - } - - resultDb.onDeleted(resultIDs); - } - - public async getPresignedUrl(fileName: string): Promise { - console.log(`[service] getPresignedUrl for ${fileName}`); - - if (env.DATA_STORAGE !== 's3') { - console.log(`[service] fs storage detected, no presigned URL needed`); - - return ''; - } - - console.log(`[service] s3 detected, generating presigned URL`); - - const { result: presignedUrl, error } = await withError((storage as S3).generatePresignedUploadUrl(fileName)); - - if (error) { - console.error(`[service] getPresignedUrl | error: ${error.message}`); - - return ''; - } - - return presignedUrl!; - } - - public async saveResult(filename: string, stream: PassThrough, presignedUrl?: string, contentLength?: string) { - if (!presignedUrl) { - console.log(`[service] saving result`); - - return await storage.saveResult(filename, stream); - } - - console.log(`[service] using direct upload via presigned URL`, presignedUrl); - - const { error } = await withError( - fetch(presignedUrl, { - method: 'PUT', - body: Readable.toWeb(stream, { - strategy: { - highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, - }, - }), - headers: { - 'Content-Type': 'application/zip', - 'Content-Length': contentLength, - }, - duplex: 'half', - } as RequestInit), - ); - - if (error) { - console.error(`[s3] saveResult | error: ${error.message}`); - throw error; - } - } - - public async saveResultDetails(resultID: string, resultDetails: ResultDetails, size: number) { - const result = await storage.saveResultDetails(resultID, resultDetails, size); - - resultDb.onCreated(result); - - return result; - } - - public async getResultsProjects(): Promise { - const { results } = await this.getResults(); - const projects = getUniqueProjectsList(results); - - const reportProjects = await this.getReportsProjects(); - - return Array.from(new Set([...projects, ...reportProjects])); - } + public static getInstance() { + console.log(`[service] get instance`); + instance[runningService] ??= new Service(); + + return instance[runningService]; + } + + public async getReports(input?: ReadReportsInput) { + console.log(`[service] getReports`); + + return reportDb.query(input); + } + + public async getReport(id: string, path?: string) { + console.log(`[service] getReport ${id}`); + + const report = reportDb.getByID(id); + + if (!report && path) { + console.warn( + `[service] getReport ${id} - not found in db, fetching from storage`, + ); + const { result: reportFromStorage, error } = await withError( + storage.readReport(id, path), + ); + + if (error) { + console.error( + `[service] getReport ${id} - error fetching from storage: ${error.message}`, + ); + throw error; + } + + if (!reportFromStorage) { + throw new Error(`report ${id} not found`); + } + + return reportFromStorage; + } - public async getResultsTags(project?: string): Promise { - const { results } = await this.getResults(project ? { project } : undefined); + return report!; + } - const notMetadataKeys = ['resultID', 'title', 'createdAt', 'size', 'sizeBytes', 'project']; - const allTags = new Set(); + private async findLatestPlaywrightVersionFromResults(resultIds: string[]) { + for (const resultId of resultIds) { + const { result: results, error } = await withError( + this.getResults({ search: resultId }), + ); - results.forEach((result) => { - Object.entries(result).forEach(([key, value]) => { - if (!notMetadataKeys.includes(key) && value !== undefined && value !== null) { - allTags.add(`${key}: ${value}`); - } - }); - }); + if (error || !results) { + continue; + } - return Array.from(allTags).sort(); - } + const [latestResult] = results.results; - public async getServerInfo(): Promise { - console.log(`[service] getServerInfo`); - const canCalculateFromCache = lifecycle.isInitialized() && reportDb.initialized && resultDb.initialized; + if (!latestResult) { + continue; + } - if (!canCalculateFromCache) { - return await storage.getServerDataInfo(); - } + const latestVersion = latestResult?.playwrightVersion; - const reports = reportDb.getAll(); - const results = resultDb.getAll(); + if (latestVersion) { + return latestVersion; + } + } + } - const getTotalSizeBytes = (entity: T) => - entity.reduce((total, item) => total + item.sizeBytes, 0); + private async findLatestPlaywrightVersion(resultIds: string[]) { + const versionFromResults = + await this.findLatestPlaywrightVersionFromResults(resultIds); - const reportsFolderSize = getTotalSizeBytes(reports); - const resultsFolderSize = getTotalSizeBytes(results); - const dataFolderSize = reportsFolderSize + resultsFolderSize; + if (versionFromResults) { + return versionFromResults; + } - return { - dataFolderSizeinMB: bytesToString(dataFolderSize), - numOfResults: results.length, - resultsFolderSizeinMB: bytesToString(resultsFolderSize), - numOfReports: reports.length, - reportsFolderSizeinMB: bytesToString(reportsFolderSize), - }; - } + // just in case version not found in results, we can try to get it from latest reports + const { result: reportsArray, error } = await withError( + this.getReports({ pagination: { limit: 10, offset: 0 } }), + ); - public async getConfig() { - if (lifecycle.isInitialized() && configCache.initialized) { - const cached = configCache.config; + if (error || !reportsArray) { + return ""; + } - if (cached) { - console.log(`[service] using cached config`); + const reportWithVersion = reportsArray.reports.find( + (report) => !!report.metadata?.playwrightVersion, + ); - return cached; - } - } + if (!reportWithVersion) { + return ""; + } - const { result, error } = await storage.readConfigFile(); + return reportWithVersion.metadata.playwrightVersion; + } - if (error) console.error(`[service] getConfig | error: ${error.message}`); + public async generateReport( + resultsIds: string[], + metadata?: ReportMetadata, + ): Promise<{ + reportId: string; + reportUrl: string; + metadata: ReportMetadata; + }> { + const version = isValidPlaywrightVersion(metadata?.playwrightVersion) + ? metadata?.playwrightVersion + : await this.findLatestPlaywrightVersion(resultsIds); - return { ...defaultConfig, ...(result ?? {}) }; - } - - public async updateConfig(config: Partial) { - console.log(`[service] updateConfig`, config); - const { result, error } = await storage.saveConfigFile(config); + const metadataWithVersion = { + ...(metadata ?? {}), + playwrightVersion: version ?? "", + }; - if (error) { - throw error; - } + const { reportId, reportPath } = await storage.generateReport( + resultsIds, + metadataWithVersion, + ); - configCache.onChanged(result); + const report = await this.getReport(reportId, reportPath); + + reportDb.onCreated(report); + + const projectPath = metadata?.project + ? `${encodeURI(metadata.project)}/` + : ""; + const reportUrl = `${serveReportRoute}/${projectPath}${reportId}/index.html`; + + return { reportId, reportUrl, metadata: metadataWithVersion }; + } + + public async deleteReports(reportIDs: string[]) { + const entries: ReportPath[] = []; + + for (const id of reportIDs) { + const report = await this.getReport(id); + + entries.push({ reportID: id, project: report.project }); + } + + const { error } = await withError(storage.deleteReports(entries)); + + if (error) { + throw error; + } + + reportDb.onDeleted(reportIDs); + } + + public async getReportsProjects(): Promise { + const { reports } = await this.getReports(); + const projects = getUniqueProjectsList(reports); + + return projects; + } + + public async getResults( + input?: ReadResultsInput, + ): Promise { + console.log(`[results service] getResults`); + + return resultDb.query(input); + } + + public async deleteResults(resultIDs: string[]): Promise { + const { error } = await withError(storage.deleteResults(resultIDs)); + + if (error) { + throw error; + } + + resultDb.onDeleted(resultIDs); + } + + public async getPresignedUrl(fileName: string): Promise { + console.log(`[service] getPresignedUrl for ${fileName}`); + + if (env.DATA_STORAGE !== "s3") { + console.log(`[service] fs storage detected, no presigned URL needed`); + + return ""; + } + + console.log(`[service] s3 detected, generating presigned URL`); + + const { result: presignedUrl, error } = await withError( + (storage as S3).generatePresignedUploadUrl(fileName), + ); + + if (error) { + console.error(`[service] getPresignedUrl | error: ${error.message}`); + + return ""; + } + + return presignedUrl!; + } + + public async saveResult( + filename: string, + stream: PassThrough, + presignedUrl?: string, + contentLength?: string, + ) { + if (!presignedUrl) { + console.log(`[service] saving result`); + + return await storage.saveResult(filename, stream); + } + + console.log( + `[service] using direct upload via presigned URL`, + presignedUrl, + ); + + const { error } = await withError( + fetch(presignedUrl, { + method: "PUT", + body: Readable.toWeb(stream, { + strategy: { + highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, + }, + }), + headers: { + "Content-Type": "application/zip", + "Content-Length": contentLength, + }, + duplex: "half", + } as RequestInit), + ); + + if (error) { + console.error(`[s3] saveResult | error: ${error.message}`); + throw error; + } + } + + public async saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, + ) { + const result = await storage.saveResultDetails( + resultID, + resultDetails, + size, + ); + + resultDb.onCreated(result); + + return result; + } + + public async getResultsProjects(): Promise { + const { results } = await this.getResults(); + const projects = getUniqueProjectsList(results); + + const reportProjects = await this.getReportsProjects(); + + return Array.from(new Set([...projects, ...reportProjects])); + } + + public async getResultsTags(project?: string): Promise { + const { results } = await this.getResults( + project ? { project } : undefined, + ); + + const notMetadataKeys = [ + "resultID", + "title", + "createdAt", + "size", + "sizeBytes", + "project", + ]; + const allTags = new Set(); + + results.forEach((result) => { + Object.entries(result).forEach(([key, value]) => { + if ( + !notMetadataKeys.includes(key) && + value !== undefined && + value !== null + ) { + allTags.add(`${key}: ${value}`); + } + }); + }); + + return Array.from(allTags).sort(); + } + + public async getServerInfo(): Promise { + console.log(`[service] getServerInfo`); + const canCalculateFromCache = + lifecycle.isInitialized() && reportDb.initialized && resultDb.initialized; + + if (!canCalculateFromCache) { + return await storage.getServerDataInfo(); + } + + const reports = reportDb.getAll(); + const results = resultDb.getAll(); + + const getTotalSizeBytes = (entity: T) => + entity.reduce((total, item) => total + item.sizeBytes, 0); + + const reportsFolderSize = getTotalSizeBytes(reports); + const resultsFolderSize = getTotalSizeBytes(results); + const dataFolderSize = reportsFolderSize + resultsFolderSize; + + return { + dataFolderSizeinMB: bytesToString(dataFolderSize), + numOfResults: results.length, + resultsFolderSizeinMB: bytesToString(resultsFolderSize), + numOfReports: reports.length, + reportsFolderSizeinMB: bytesToString(reportsFolderSize), + }; + } + + public async getConfig() { + if (lifecycle.isInitialized() && configCache.initialized) { + const cached = configCache.config; + + if (cached) { + console.log(`[service] using cached config`); + + return cached; + } + } + + const { result, error } = await storage.readConfigFile(); + + if (error) console.error(`[service] getConfig | error: ${error.message}`); + + return { ...defaultConfig, ...(result ?? {}) }; + } + + public async updateConfig(config: Partial) { + console.log(`[service] updateConfig`, config); + const { result, error } = await storage.saveConfigFile(config); + + if (error) { + throw error; + } + + configCache.onChanged(result); - return result; - } + return result; + } } export const service = Service.getInstance(); diff --git a/app/lib/storage/fs.ts b/app/lib/storage/fs.ts index 516c1a2a..d7944c3b 100644 --- a/app/lib/storage/fs.ts +++ b/app/lib/storage/fs.ts @@ -1,382 +1,486 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { randomUUID } from 'node:crypto'; -import { createWriteStream, type Dirent, type Stats } from 'node:fs'; -import { pipeline } from 'node:stream/promises'; -import { PassThrough } from 'node:stream'; - -import getFolderSize from 'get-folder-size'; - -import { bytesToString } from './format'; -import { - APP_CONFIG, - DATA_FOLDER, - DEFAULT_STREAM_CHUNK_SIZE, - REPORT_METADATA_FILE, - REPORTS_FOLDER, - REPORTS_PATH, - RESULTS_FOLDER, - TMP_FOLDER, -} from './constants'; -import { processBatch } from './batch'; -import { createDirectory } from './folders'; - -import { defaultConfig, isConfigValid, noConfigErr } from '@/app/lib/config'; -import { parse } from '@/app/lib/parser'; -import { generatePlaywrightReport } from '@/app/lib/pw'; -import { withError } from '@/app/lib/withError'; -import { serveReportRoute } from '@/app/lib/constants'; +import { randomUUID } from "node:crypto"; +import { createWriteStream, type Dirent, type Stats } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import type { PassThrough } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +import getFolderSize from "get-folder-size"; +import { defaultConfig, isConfigValid, noConfigErr } from "@/app/lib/config"; +import { serveReportRoute } from "@/app/lib/constants"; +import { parse } from "@/app/lib/parser"; +import { generatePlaywrightReport } from "@/app/lib/pw"; +import type { + ReadReportsOutput, + ReportHistory, + ReportMetadata, + ReportPath, + Result, + ResultDetails, + ServerDataInfo, + Storage, +} from "@/app/lib/storage"; +import { withError } from "@/app/lib/withError"; +import type { SiteWhiteLabelConfig } from "@/app/types"; +import { processBatch } from "./batch"; import { - type Storage, - type Result, - type ServerDataInfo, - type ResultDetails, - ReadReportsOutput, - ReportMetadata, - ReportHistory, - ReportPath, -} from '@/app/lib/storage'; -import { SiteWhiteLabelConfig } from '@/app/types'; + APP_CONFIG, + DATA_FOLDER, + DEFAULT_STREAM_CHUNK_SIZE, + REPORT_METADATA_FILE, + REPORTS_FOLDER, + REPORTS_PATH, + RESULTS_FOLDER, + TMP_FOLDER, +} from "./constants"; +import { createDirectory } from "./folders"; +import { bytesToString } from "./format"; async function createDirectoriesIfMissing() { - await createDirectory(RESULTS_FOLDER); - await createDirectory(REPORTS_FOLDER); - await createDirectory(TMP_FOLDER); + await createDirectory(RESULTS_FOLDER); + await createDirectory(REPORTS_FOLDER); + await createDirectory(TMP_FOLDER); } const getSizeInMb = async (dir: string) => { - const sizeBytes = await getFolderSize.loose(dir); + const sizeBytes = await getFolderSize.loose(dir); - return bytesToString(sizeBytes); + return bytesToString(sizeBytes); }; export async function getServerDataInfo(): Promise { - await createDirectoriesIfMissing(); - const dataFolderSizeinMB = await getSizeInMb(DATA_FOLDER); - const resultsCount = await getResultsCount(); - const resultsFolderSizeinMB = await getSizeInMb(RESULTS_FOLDER); - const { total: reportsCount } = await readReports(); - const reportsFolderSizeinMB = await getSizeInMb(REPORTS_FOLDER); - - return { - dataFolderSizeinMB, - numOfResults: resultsCount, - resultsFolderSizeinMB, - numOfReports: reportsCount, - reportsFolderSizeinMB, - }; + await createDirectoriesIfMissing(); + const dataFolderSizeinMB = await getSizeInMb(DATA_FOLDER); + const resultsCount = await getResultsCount(); + const resultsFolderSizeinMB = await getSizeInMb(RESULTS_FOLDER); + const { total: reportsCount } = await readReports(); + const reportsFolderSizeinMB = await getSizeInMb(REPORTS_FOLDER); + + return { + dataFolderSizeinMB, + numOfResults: resultsCount, + resultsFolderSizeinMB, + numOfReports: reportsCount, + reportsFolderSizeinMB, + }; } export async function readFile(targetPath: string, contentType: string | null) { - return await fs.readFile(path.join(REPORTS_FOLDER, targetPath), { - encoding: contentType === 'text/html' ? 'utf-8' : null, - }); + return await fs.readFile(path.join(REPORTS_FOLDER, targetPath), { + encoding: contentType === "text/html" ? "utf-8" : null, + }); } async function getResultsCount() { - const files = await fs.readdir(RESULTS_FOLDER); - const zipFilesCount = files.filter((file) => file.endsWith('.zip')); + const files = await fs.readdir(RESULTS_FOLDER); + const zipFilesCount = files.filter((file) => file.endsWith(".zip")); - return zipFilesCount.length; + return zipFilesCount.length; } export async function readResults() { - await createDirectoriesIfMissing(); - const files = await fs.readdir(RESULTS_FOLDER); - - const stats = await processBatch( - {}, - files.filter((file) => file.endsWith('.json')), - 20, - async (file) => { - const filePath = path.join(RESULTS_FOLDER, file); - - const stat = await fs.stat(filePath); - - const sizeBytes = await getFolderSize.loose(filePath.replace('.json', '.zip')); - - const size = bytesToString(sizeBytes); - - return Object.assign(stat, { filePath, size, sizeBytes }); - }, - ); - - const results = await processBatch< - Stats & { - filePath: string; - size: string; - sizeBytes: number; - }, - Result - >({}, stats, 10, async (entry) => { - const content = await fs.readFile(entry.filePath, 'utf-8'); - - return { - size: entry.size, - sizeBytes: entry.sizeBytes, - ...JSON.parse(content), - }; - }); - - return { - results, - total: results.length, - }; + await createDirectoriesIfMissing(); + const files = await fs.readdir(RESULTS_FOLDER); + + const stats = await processBatch< + string, + Stats & { filePath: string; size: string; sizeBytes: number } + >( + {}, + files.filter((file) => file.endsWith(".json")), + 20, + async (file) => { + const filePath = path.join(RESULTS_FOLDER, file); + + const stat = await fs.stat(filePath); + + const sizeBytes = await getFolderSize.loose( + filePath.replace(".json", ".zip"), + ); + + const size = bytesToString(sizeBytes); + + return Object.assign(stat, { filePath, size, sizeBytes }); + }, + ); + + const results = await processBatch< + Stats & { + filePath: string; + size: string; + sizeBytes: number; + }, + Result + >({}, stats, 10, async (entry) => { + const content = await fs.readFile(entry.filePath, "utf-8"); + + return { + size: entry.size, + sizeBytes: entry.sizeBytes, + ...JSON.parse(content), + }; + }); + + return { + results, + total: results.length, + }; } function isMissingFileError(error?: Error | null) { - return error?.message.includes('ENOENT'); + return error?.message.includes("ENOENT"); } -async function readOrParseReportMetadata(id: string, projectName: string): Promise { - const { result: metadataContent, error: metadataError } = await withError( - readFile(path.join(projectName, id, REPORT_METADATA_FILE), 'utf-8'), - ); - - if (metadataError) console.error(`failed to read metadata for ${id}: ${metadataError.message}`); - - const metadata = metadataContent && !metadataError ? JSON.parse(metadataContent.toString()) : {}; - - if (!isMissingFileError(metadataError)) { - return metadata; - } - - console.log(`metadata file not found for ${id}, creating new metadata`); - try { - const parsed = await parseReportMetadata(id, path.join(REPORTS_FOLDER, projectName, id), { - project: projectName, - reportID: id, - }); - - console.log(`parsed metadata for ${id}`); - - await saveReportMetadata(path.join(REPORTS_FOLDER, projectName, id), parsed); - - Object.assign(metadata, parsed); - } catch (e) { - console.error(`failed to create metadata for ${id}: ${(e as Error).message}`); - } +async function readOrParseReportMetadata( + id: string, + projectName: string, +): Promise { + const { result: metadataContent, error: metadataError } = await withError( + readFile(path.join(projectName, id, REPORT_METADATA_FILE), "utf-8"), + ); + + if (metadataError) + console.error( + `failed to read metadata for ${id}: ${metadataError.message}`, + ); + + const metadata = + metadataContent && !metadataError + ? JSON.parse(metadataContent.toString()) + : {}; + + if (!isMissingFileError(metadataError)) { + return metadata; + } + + console.log(`metadata file not found for ${id}, creating new metadata`); + try { + const parsed = await parseReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + { + project: projectName, + reportID: id, + }, + ); + + console.log(`parsed metadata for ${id}`); + + await saveReportMetadata( + path.join(REPORTS_FOLDER, projectName, id), + parsed, + ); + + Object.assign(metadata, parsed); + } catch (e) { + console.error( + `failed to create metadata for ${id}: ${(e as Error).message}`, + ); + } + + return metadata; +} - return metadata; +export async function readReport( + reportID: string, + reportPath: string, +): Promise { + await createDirectoriesIfMissing(); + + console.log(`[fs] reading report ${reportID} metadata`); + + const { result: metadataContent, error: metadataError } = await withError( + readFile(path.join(reportPath, REPORT_METADATA_FILE), "utf-8"), + ); + + if (metadataError) { + console.error( + `[fs] failed to read metadata for ${reportID}: ${metadataError.message}`, + ); + + return null; + } + + const metadata = metadataContent + ? JSON.parse(metadataContent.toString()) + : {}; + + return { + reportID, + project: metadata.project || "", + createdAt: new Date(metadata.createdAt), + size: metadata.size || "", + sizeBytes: metadata.sizeBytes || 0, + reportUrl: metadata.reportUrl || "", + ...metadata, + } as ReportHistory; } export async function readReports(): Promise { - await createDirectoriesIfMissing(); - const entries = await fs.readdir(REPORTS_FOLDER, { withFileTypes: true, recursive: true }); - - const reportEntries = entries.filter( - (entry) => !entry.isDirectory() && entry.name === 'index.html' && !(entry as any).path.endsWith('trace'), - ); - - const stats = await processBatch( - {}, - reportEntries, - 20, - async (file) => { - const stat = await fs.stat((file as any).path); - - return Object.assign(stat, { filePath: (file as any).path, createdAt: stat.birthtime }); - }, - ); - - const reports = await processBatch( - {}, - stats, - 10, - async (file) => { - const id = path.basename(file.filePath); - const reportPath = path.dirname(file.filePath); - const parentDir = path.basename(reportPath); - const sizeBytes = await getFolderSize.loose(path.join(reportPath, id)); - const size = bytesToString(sizeBytes); - - const projectName = parentDir === REPORTS_PATH ? '' : parentDir; - - const metadata = await readOrParseReportMetadata(id, projectName); - - return { - reportID: id, - project: projectName, - createdAt: file.birthtime, - size, - sizeBytes, - reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ''}/${id}/index.html`, - ...metadata, - } as ReportHistory; - }, - ); - - return { reports: reports, total: reports.length }; + await createDirectoriesIfMissing(); + const entries = await fs.readdir(REPORTS_FOLDER, { + withFileTypes: true, + recursive: true, + }); + + const reportEntries = entries.filter( + (entry) => + !entry.isDirectory() && + entry.name === "index.html" && + !(entry as any).path.endsWith("trace"), + ); + + const stats = await processBatch< + Dirent, + Stats & { filePath: string; createdAt: Date } + >({}, reportEntries, 20, async (file) => { + const stat = await fs.stat((file as any).path); + + return Object.assign(stat, { + filePath: (file as any).path, + createdAt: stat.birthtime, + }); + }); + + const reports = await processBatch< + Stats & { filePath: string; createdAt: Date }, + ReportHistory + >({}, stats, 10, async (file) => { + const id = path.basename(file.filePath); + const reportPath = path.dirname(file.filePath); + const parentDir = path.basename(reportPath); + const sizeBytes = await getFolderSize.loose(path.join(reportPath, id)); + const size = bytesToString(sizeBytes); + + const projectName = parentDir === REPORTS_PATH ? "" : parentDir; + + const metadata = await readOrParseReportMetadata(id, projectName); + + return { + reportID: id, + project: projectName, + createdAt: file.birthtime, + size, + sizeBytes, + reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ""}/${id}/index.html`, + ...metadata, + } as ReportHistory; + }); + + return { reports: reports, total: reports.length }; } export async function deleteResults(resultsIds: string[]) { - await Promise.allSettled(resultsIds.map((id) => deleteResult(id))); + await Promise.allSettled(resultsIds.map((id) => deleteResult(id))); } export async function deleteResult(resultId: string) { - const resultPath = path.join(RESULTS_FOLDER, resultId); + const resultPath = path.join(RESULTS_FOLDER, resultId); - await Promise.allSettled([fs.unlink(`${resultPath}.json`), fs.unlink(`${resultPath}.zip`)]); + await Promise.allSettled([ + fs.unlink(`${resultPath}.json`), + fs.unlink(`${resultPath}.zip`), + ]); } export async function deleteReports(reports: ReportPath[]) { - const paths = reports.map((report) => (report.project ? `${report.project}/${report.reportID}` : report.reportID)); + const paths = reports.map((report) => + report.project ? `${report.project}/${report.reportID}` : report.reportID, + ); - await processBatch(undefined, paths, 10, async (path) => { - await deleteReport(path); - }); + await processBatch(undefined, paths, 10, async (path) => { + await deleteReport(path); + }); } export async function deleteReport(reportId: string) { - const reportPath = path.join(REPORTS_FOLDER, reportId); + const reportPath = path.join(REPORTS_FOLDER, reportId); - await fs.rm(reportPath, { recursive: true, force: true }); + await fs.rm(reportPath, { recursive: true, force: true }); } export async function saveResult(filename: string, stream: PassThrough) { - await createDirectoriesIfMissing(); - const resultPath = path.join(RESULTS_FOLDER, filename); + await createDirectoriesIfMissing(); + const resultPath = path.join(RESULTS_FOLDER, filename); - const writeable = createWriteStream(resultPath, { - encoding: 'binary', - highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, - }); + const writeable = createWriteStream(resultPath, { + encoding: "binary", + highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, + }); - const { error: writeStreamError } = await withError(pipeline(stream, writeable)); + const { error: writeStreamError } = await withError( + pipeline(stream, writeable), + ); - if (writeStreamError) { - throw new Error(`failed stream pipeline: ${writeStreamError.message}`); - } + if (writeStreamError) { + throw new Error(`failed stream pipeline: ${writeStreamError.message}`); + } } -export async function saveResultDetails(resultID: string, resultDetails: ResultDetails, size: number): Promise { - await createDirectoriesIfMissing(); - - const metaData = { - resultID, - createdAt: new Date().toISOString(), - project: resultDetails?.project ?? '', - ...resultDetails, - size: bytesToString(size), - sizeBytes: size, - }; - - const { error: writeJsonError } = await withError( - fs.writeFile(path.join(RESULTS_FOLDER, `${resultID}.json`), JSON.stringify(metaData, null, 2), { - encoding: 'utf-8', - }), - ); - - if (writeJsonError) { - throw new Error(`failed to save result ${resultID} json file: ${writeJsonError.message}`); - } - - return metaData as Result; +export async function saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, +): Promise { + await createDirectoriesIfMissing(); + + const metaData = { + resultID, + createdAt: new Date().toISOString(), + project: resultDetails?.project ?? "", + ...resultDetails, + size: bytesToString(size), + sizeBytes: size, + }; + + const { error: writeJsonError } = await withError( + fs.writeFile( + path.join(RESULTS_FOLDER, `${resultID}.json`), + JSON.stringify(metaData, null, 2), + { + encoding: "utf-8", + }, + ), + ); + + if (writeJsonError) { + throw new Error( + `failed to save result ${resultID} json file: ${writeJsonError.message}`, + ); + } + + return metaData as Result; } -export async function generateReport(resultsIds: string[], metadata?: ReportMetadata) { - await createDirectoriesIfMissing(); - - const reportId = randomUUID(); - const tempFolder = path.join(TMP_FOLDER, reportId); - - await fs.mkdir(tempFolder, { recursive: true }); - - try { - for (const id of resultsIds) { - await fs.copyFile(path.join(RESULTS_FOLDER, `${id}.zip`), path.join(tempFolder, `${id}.zip`)); - } - const generated = await generatePlaywrightReport(reportId, metadata!); - const info = await parseReportMetadata(reportId, generated.reportPath, metadata); - - await saveReportMetadata(generated.reportPath, info); - - return reportId; - } finally { - await fs.rm(tempFolder, { recursive: true, force: true }); - } +export async function generateReport( + resultsIds: string[], + metadata?: ReportMetadata, +) { + await createDirectoriesIfMissing(); + + const reportId = randomUUID(); + const tempFolder = path.join(TMP_FOLDER, reportId); + + await fs.mkdir(tempFolder, { recursive: true }); + + try { + for (const id of resultsIds) { + await fs.copyFile( + path.join(RESULTS_FOLDER, `${id}.zip`), + path.join(tempFolder, `${id}.zip`), + ); + } + const generated = await generatePlaywrightReport(reportId, metadata!); + const info = await parseReportMetadata( + reportId, + generated.reportPath, + metadata, + ); + + await saveReportMetadata(generated.reportPath, info); + + return { reportId, reportPath: generated.reportPath }; + } finally { + await fs.rm(tempFolder, { recursive: true, force: true }); + } } async function parseReportMetadata( - reportID: string, - reportPath: string, - metadata?: ReportMetadata, + reportID: string, + reportPath: string, + metadata?: ReportMetadata, ): Promise { - const html = await fs.readFile(path.join(reportPath, 'index.html'), 'utf-8'); - const info = await parse(html as string); - - const content = Object.assign( - info, - { - reportID, - createdAt: new Date().toISOString(), - }, - metadata ?? {}, - ); - - return content; + const html = await fs.readFile(path.join(reportPath, "index.html"), "utf-8"); + const info = await parse(html as string); + + const content = Object.assign( + info, + { + reportID, + createdAt: new Date().toISOString(), + }, + metadata ?? {}, + ); + + return content; } async function saveReportMetadata(reportPath: string, info: ReportMetadata) { - return fs.writeFile(path.join(reportPath, REPORT_METADATA_FILE), JSON.stringify(info, null, 2), { - encoding: 'utf-8', - }); + return fs.writeFile( + path.join(reportPath, REPORT_METADATA_FILE), + JSON.stringify(info, null, 2), + { + encoding: "utf-8", + }, + ); } async function readConfigFile() { - const { error: accessConfigError } = await withError(fs.access(APP_CONFIG)); + const { error: accessConfigError } = await withError(fs.access(APP_CONFIG)); - if (accessConfigError) { - return { result: defaultConfig, error: new Error(noConfigErr) }; - } + if (accessConfigError) { + return { result: defaultConfig, error: new Error(noConfigErr) }; + } - const { result, error } = await withError(fs.readFile(APP_CONFIG, 'utf-8')); + const { result, error } = await withError(fs.readFile(APP_CONFIG, "utf-8")); - if (error || !result) { - return { error }; - } + if (error || !result) { + return { error }; + } - try { - const parsed = JSON.parse(result); + try { + const parsed = JSON.parse(result); - const isValid = isConfigValid(parsed); + const isValid = isConfigValid(parsed); - return isValid ? { result: parsed, error: null } : { error: new Error('invalid config') }; - } catch (e) { - return { error: new Error(`failed to parse config: ${e instanceof Error ? e.message : e}`) }; - } + return isValid + ? { result: parsed, error: null } + : { error: new Error("invalid config") }; + } catch (e) { + return { + error: new Error( + `failed to parse config: ${e instanceof Error ? e.message : e}`, + ), + }; + } } async function saveConfigFile(config: Partial) { - const { result: existingConfig, error: configError } = await readConfigFile(); + const { result: existingConfig, error: configError } = await readConfigFile(); - const isConfigFailed = !!configError && configError?.message !== noConfigErr && !existingConfig; + const isConfigFailed = + !!configError && configError?.message !== noConfigErr && !existingConfig; - if (isConfigFailed) { - console.error(`failed to read existing config: ${configError.message}`); - } + if (isConfigFailed) { + console.error(`failed to read existing config: ${configError.message}`); + } - const previousConfig = existingConfig ?? defaultConfig; - const uploadConfig = { ...previousConfig, ...config }; + const previousConfig = existingConfig ?? defaultConfig; + const uploadConfig = { ...previousConfig, ...config }; - const { error } = await withError(fs.writeFile(APP_CONFIG, JSON.stringify(uploadConfig, null, 2), { flag: 'w+' })); + const { error } = await withError( + fs.writeFile(APP_CONFIG, JSON.stringify(uploadConfig, null, 2), { + flag: "w+", + }), + ); - return { - result: uploadConfig, - error, - }; + return { + result: uploadConfig, + error, + }; } export const FS: Storage = { - getServerDataInfo, - readFile, - readResults, - readReports, - deleteResults, - deleteReports, - saveResult, - saveResultDetails, - generateReport, - readConfigFile, - saveConfigFile, + getServerDataInfo, + readFile, + readResults, + readReports, + readReport, + deleteResults, + deleteReports, + saveResult, + saveResultDetails, + generateReport, + readConfigFile, + saveConfigFile, }; diff --git a/app/lib/storage/s3.ts b/app/lib/storage/s3.ts index f3f9723a..7928df81 100644 --- a/app/lib/storage/s3.ts +++ b/app/lib/storage/s3.ts @@ -1,1098 +1,1359 @@ -import { randomUUID, type UUID } from 'node:crypto'; -import { createWriteStream, createReadStream } from 'node:fs'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { PassThrough, Readable } from 'node:stream'; +import { randomUUID, type UUID } from "node:crypto"; +import { createReadStream, createWriteStream } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import type { PassThrough, Readable } from "node:stream"; import { - S3Client, - HeadBucketCommand, - CreateBucketCommand, - PutObjectCommand, - GetObjectCommand, - DeleteObjectCommand, - ListObjectsV2Command, - HeadObjectCommand, - CreateMultipartUploadCommand, - UploadPartCommand, - CompleteMultipartUploadCommand, - AbortMultipartUploadCommand, - type _Object, -} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; - -import { processBatch } from './batch'; + type _Object, + AbortMultipartUploadCommand, + CompleteMultipartUploadCommand, + CreateBucketCommand, + CreateMultipartUploadCommand, + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + HeadObjectCommand, + ListObjectsV2Command, + PutObjectCommand, + S3Client, + UploadPartCommand, +} from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { env } from "@/app/config/env"; +import { defaultConfig, isConfigValid } from "@/app/lib/config"; +import { serveReportRoute } from "@/app/lib/constants"; +import { parse } from "@/app/lib/parser"; +import { generatePlaywrightReport } from "@/app/lib/pw"; +import { withError } from "@/app/lib/withError"; +import type { SiteWhiteLabelConfig } from "@/app/types"; +import { processBatch } from "./batch"; import { - Result, - Report, - ResultDetails, - ServerDataInfo, - isReportHistory, - ReadReportsOutput, - ReadResultsOutput, - ReportHistory, - ReportMetadata, - Storage, - ReportPath, -} from './types'; -import { bytesToString } from './format'; + APP_CONFIG_S3, + DATA_FOLDER, + DATA_PATH, + REPORT_METADATA_FILE, + REPORTS_BUCKET, + REPORTS_FOLDER, + REPORTS_PATH, + RESULTS_BUCKET, + TMP_FOLDER, +} from "./constants"; +import { getFileReportID } from "./file"; +import { bytesToString } from "./format"; import { - REPORTS_FOLDER, - TMP_FOLDER, - REPORTS_BUCKET, - RESULTS_BUCKET, - REPORTS_PATH, - REPORT_METADATA_FILE, - APP_CONFIG_S3, - DATA_PATH, - DATA_FOLDER, -} from './constants'; -import { getFileReportID } from './file'; - -import { parse } from '@/app/lib/parser'; -import { serveReportRoute } from '@/app/lib/constants'; -import { generatePlaywrightReport } from '@/app/lib/pw'; -import { withError } from '@/app/lib/withError'; -import { env } from '@/app/config/env'; -import { SiteWhiteLabelConfig } from '@/app/types'; -import { defaultConfig, isConfigValid } from '@/app/lib/config'; + isReportHistory, + type ReadReportsOutput, + type ReadResultsOutput, + type Report, + type ReportHistory, + type ReportMetadata, + type ReportPath, + type Result, + type ResultDetails, + type ServerDataInfo, + type Storage, +} from "./types"; const createClient = () => { - const endPoint = env.S3_ENDPOINT; - const accessKey = env.S3_ACCESS_KEY; - const secretKey = env.S3_SECRET_KEY; - const port = env.S3_PORT; - const region = env.S3_REGION; - - if (!endPoint) { - throw new Error('S3_ENDPOINT is required'); - } - - if (!accessKey) { - throw new Error('S3_ACCESS_KEY is required'); - } - - if (!secretKey) { - throw new Error('S3_SECRET_KEY is required'); - } - - console.log('[s3] creating client'); - - const protocol = 'https://'; - const endpointUrl = port ? `${protocol}${endPoint}:${port}` : `${protocol}${endPoint}`; - - const client = new S3Client({ - region: region || 'us-east-1', - endpoint: endpointUrl, - credentials: { - accessKeyId: accessKey, - secretAccessKey: secretKey, - }, - forcePathStyle: true, // required for S3-compatible services like Minio - }); - - return client; + const endPoint = env.S3_ENDPOINT; + const accessKey = env.S3_ACCESS_KEY; + const secretKey = env.S3_SECRET_KEY; + const port = env.S3_PORT; + const region = env.S3_REGION; + + if (!endPoint) { + throw new Error("S3_ENDPOINT is required"); + } + + if (!accessKey) { + throw new Error("S3_ACCESS_KEY is required"); + } + + if (!secretKey) { + throw new Error("S3_SECRET_KEY is required"); + } + + console.log("[s3] creating client"); + + const protocol = "https://"; + const endpointUrl = port + ? `${protocol}${endPoint}:${port}` + : `${protocol}${endPoint}`; + + const client = new S3Client({ + region: region || "us-east-1", + endpoint: endpointUrl, + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretKey, + }, + forcePathStyle: true, // required for S3-compatible services like Minio + }); + + return client; }; export class S3 implements Storage { - private static instance: S3; - private readonly client: S3Client; - private readonly bucket: string; - private readonly batchSize: number; - - private constructor() { - this.client = createClient(); - this.bucket = env.S3_BUCKET; - this.batchSize = env.S3_BATCH_SIZE; - } - - public static getInstance() { - if (!S3.instance) { - S3.instance = new S3(); - } - - return S3.instance; - } - - private async ensureBucketExist() { - const { error } = await withError(this.client.send(new HeadBucketCommand({ Bucket: this.bucket }))); - - if (!error) { - return; - } - - if (error.name === 'NotFound') { - console.log(`[s3] bucket ${this.bucket} does not exist, creating...`); - - const { error } = await withError( - this.client.send( - new CreateBucketCommand({ - Bucket: this.bucket, - }), - ), - ); - - if (error) { - console.error('[s3] failed to create bucket:', error); - } - } - - console.error('[s3] failed to check that bucket exist:', error); - } - - private async write(dir: string, files: { name: string; content: Readable | Buffer | string; size?: number }[]) { - await this.ensureBucketExist(); - for (const file of files) { - const filePath = path.join(dir, file.name); - - console.log(`[s3] writing ${filePath}`); - - const content = typeof file.content === 'string' ? Buffer.from(file.content) : file.content; - - await this.client.send( - new PutObjectCommand({ - Bucket: this.bucket, - Key: path.normalize(filePath), - Body: content, - }), - ); - } - } - - private async read(targetPath: string, contentType?: string | null) { - await this.ensureBucketExist(); - console.log(`[s3] read ${targetPath}`); - - const remotePath = targetPath.includes(REPORTS_BUCKET) ? targetPath : `${REPORTS_BUCKET}/${targetPath}`; - - console.log(`[s3] reading from remote path: ${remotePath}`); - - const { result: response, error } = await withError( - this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: remotePath, - }), - ), - ); - - if (error ?? !response?.Body) { - return { result: null, error }; - } - - const stream = response.Body as Readable; - - const readStream = new Promise((resolve, reject) => { - const chunks: Uint8Array[] = []; - - stream.on('data', (chunk: Uint8Array) => { - chunks.push(chunk); - }); - - stream.on('end', () => { - const fullContent = Buffer.concat(chunks); - - resolve(fullContent); - }); - - stream.on('error', (error) => { - console.error(`[s3] failed to read stream: ${error.message}`); - reject(error); - }); - }); - - const { result, error: readError } = await withError(readStream); - - return { - result: contentType === 'text/html' ? result?.toString('utf-8') : result, - error: error ?? readError ?? null, - }; - } - - async clear(...path: string[]) { - console.log(`[s3] clearing ${path}`); - // avoid using "removeObjects" as it is not supported by every S3-compatible provider - // for example, Google Cloud Storage. - await processBatch(this, path, this.batchSize, async (object) => { - await this.client.send( - new DeleteObjectCommand({ - Bucket: this.bucket, - Key: object, - }), - ); - }); - } - - async getFolderSize(folderPath: string): Promise<{ size: number; resultCount: number; indexCount: number }> { - let resultCount = 0; - let indexCount = 0; - let totalSize = 0; - - let continuationToken: string | undefined; - - do { - const response = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: folderPath, - ContinuationToken: continuationToken, - }), - ); - - for (const obj of response.Contents ?? []) { - if (obj.Key?.endsWith('.zip')) { - resultCount += 1; - } - - if (obj.Key?.endsWith('index.html') && !obj.Key.includes('/trace/index.html')) { - indexCount += 1; - } - - totalSize += obj?.Size ?? 0; - } - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - } while (continuationToken); - - return { size: totalSize, resultCount, indexCount }; - } - - async getServerDataInfo(): Promise { - await this.ensureBucketExist(); - console.log('[s3] getting server data'); - - const [results, reports] = await Promise.all([ - this.getFolderSize(RESULTS_BUCKET), - this.getFolderSize(REPORTS_BUCKET), - ]); - - const dataSize = results.size + reports.size; - - return { - dataFolderSizeinMB: bytesToString(dataSize), - numOfResults: results.resultCount, - resultsFolderSizeinMB: bytesToString(results.size), - numOfReports: reports.indexCount, - reportsFolderSizeinMB: bytesToString(reports.size), - }; - } - - async readFile(targetPath: string, contentType: string | null): Promise { - console.log(`[s3] reading ${targetPath} | ${contentType}`); - const { result, error } = await this.read(targetPath, contentType); - - if (error) { - console.error(`[s3] failed to read file ${targetPath}: ${error.message}`); - throw new Error(`[s3] failed to read file: ${error.message}`); - } - - return result!; - } - - async readResults(): Promise { - await this.ensureBucketExist(); - - console.log('[s3] reading results'); - - const jsonFiles: _Object[] = []; - const resultSizes = new Map(); - - let continuationToken: string | undefined; - - do { - const response = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: RESULTS_BUCKET, - ContinuationToken: continuationToken, - }), - ); - - for (const file of response.Contents ?? []) { - if (!file?.Key) { - continue; - } - - if (file.Key.endsWith('.zip')) { - const resultID = path.basename(file.Key, '.zip'); - - resultSizes.set(resultID, file.Size ?? 0); - } - - if (file.Key.endsWith('.json')) { - jsonFiles.push(file); - } - } - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - } while (continuationToken); - - console.log(`[s3] found ${jsonFiles.length} json files`); - - if (!jsonFiles) { - return { - results: [], - total: 0, - }; - } - - const results = await processBatch<_Object, Result>(this, jsonFiles, this.batchSize, async (file) => { - console.log(`[s3.batch] reading result: ${JSON.stringify(file)}`); - const response = await this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: file.Key!, - }), - ); - - const stream = response.Body as Readable; - let jsonString = ''; - - for await (const chunk of stream) { - jsonString += chunk.toString(); - } - - const parsed = JSON.parse(jsonString); - - return parsed; - }); - - return { - results: results.map((result) => { - const sizeBytes = resultSizes.get(result.resultID) ?? 0; - - return { - ...result, - sizeBytes, - size: result.size ?? bytesToString(sizeBytes), - }; - }) as Result[], - total: results.length, - }; - } - - async readReports(): Promise { - await this.ensureBucketExist(); - - console.log(`[s3] reading reports from external storage`); - - const reports: Report[] = []; - const reportSizes = new Map(); - - let continuationToken: string | undefined; - - do { - const response = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: REPORTS_BUCKET, - ContinuationToken: continuationToken, - }), - ); - - for (const file of response.Contents ?? []) { - if (!file?.Key) { - continue; - } - - const reportID = getFileReportID(file.Key); - - const newSize = (reportSizes.get(reportID) ?? 0) + (file.Size ?? 0); - - reportSizes.set(reportID, newSize); - - if (!file.Key.endsWith('index.html') || file.Key.includes('trace')) { - continue; - } - - const dir = path.dirname(file.Key); - const id = path.basename(dir); - const parentDir = path.basename(path.dirname(dir)); - - const projectName = parentDir === REPORTS_PATH ? '' : parentDir; - - const report = { - reportID: id, - project: projectName, - createdAt: file.LastModified ?? new Date(), - reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ''}/${id}/index.html`, - size: '', - sizeBytes: 0, - }; - - reports.push(report); - } - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - } while (continuationToken); - - const withMetadata = await this.getReportsMetadata(reports as ReportHistory[]); - - return { - reports: withMetadata.map((report) => { - const sizeBytes = reportSizes.get(report.reportID) ?? 0; - - return { - ...report, - sizeBytes, - size: bytesToString(sizeBytes), - }; - }), - total: withMetadata.length, - }; - } - - async getReportsMetadata(reports: ReportHistory[]): Promise { - return await processBatch(this, reports, this.batchSize, async (report) => { - console.log(`[s3.batch] reading report ${report.reportID} metadata`); - - const { result: metadata, error: metadataError } = await withError( - this.readOrParseReportMetadata(report.reportID, report.project), - ); - - if (metadataError) { - console.error(`[s3] failed to read or create metadata for ${report.reportID}: ${metadataError.message}`); - - return report; - } - - if (!metadata) { - return report; - } - - return Object.assign(metadata, report); - }); - } - - async readOrParseReportMetadata(id: string, projectName: string): Promise { - const { result: metadataContent, error: metadataError } = await withError( - this.readFile(path.join(REPORTS_BUCKET, projectName, id, REPORT_METADATA_FILE), 'utf-8'), - ); - - if (metadataError) console.error(`[s3] failed to read metadata for ${id}: ${metadataError.message}`); - - const metadata = metadataContent && !metadataError ? JSON.parse(metadataContent.toString()) : {}; - - if (isReportHistory(metadata)) { - console.log(`metadata found for report ${id}`); - - return metadata; - } - - console.log(`metadata file not found for ${id}, creating new metadata`); - try { - const { result: htmlContent, error: htmlError } = await withError( - this.readFile(path.join(REPORTS_BUCKET, projectName, id, 'index.html'), 'utf-8'), - ); - - if (htmlError) console.error(`[s3] failed to read index.html for ${id}: ${htmlError.message}`); - - const created = await this.parseReportMetadata( - id, - path.join(REPORTS_FOLDER, projectName, id), - { - project: projectName, - reportID: id, - }, - htmlContent?.toString(), - ); - - console.log(`metadata object created for ${id}: ${JSON.stringify(created)}`); - - await this.saveReportMetadata(id, path.join(REPORTS_FOLDER, projectName, id), created); - - Object.assign(metadata, created); - } catch (e) { - console.error(`failed to create metadata for ${id}: ${(e as Error).message}`); - } - - return metadata; - } - - async deleteResults(resultIDs: string[]): Promise { - const objects = resultIDs.flatMap((id) => [`${RESULTS_BUCKET}/${id}.json`, `${RESULTS_BUCKET}/${id}.zip`]); - - await withError(this.clear(...objects)); - } - - private async getReportObjects(reportsIDs: string[]): Promise { - const files: string[] = []; - - let continuationToken: string | undefined; - - do { - const response = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: REPORTS_BUCKET, - ContinuationToken: continuationToken, - }), - ); - - for (const file of response.Contents ?? []) { - if (!file?.Key) { - continue; - } - - const reportID = path.basename(path.dirname(file.Key)); - - if (reportsIDs.includes(reportID)) { - files.push(file.Key); - } - } - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - } while (continuationToken); - - return files; - } - - async deleteReports(reports: ReportPath[]): Promise { - const ids = reports.map((r) => r.reportID); - const objects = await this.getReportObjects(ids); - - await withError(this.clear(...objects)); - } - - async generatePresignedUploadUrl(fileName: string) { - await this.ensureBucketExist(); - const objectKey = path.join(RESULTS_BUCKET, fileName); - const expiry = 30 * 60; // 30 minutes - - const command = new PutObjectCommand({ - Bucket: this.bucket, - Key: objectKey, - }); - - return await getSignedUrl(this.client, command, { expiresIn: expiry }); - } - - async saveResult(filename: string, stream: PassThrough) { - await this.ensureBucketExist(); - - const chunkSizeMB = env.S3_MULTIPART_CHUNK_SIZE_MB; - const chunkSize = chunkSizeMB * 1024 * 1024; // bytes - - console.log(`[s3] starting multipart upload for ${filename} with chunk size ${chunkSizeMB}MB`); - - const remotePath = path.join(RESULTS_BUCKET, filename); - - const { UploadId: uploadID } = await this.client.send( - new CreateMultipartUploadCommand({ - Bucket: this.bucket, - Key: remotePath, - }), - ); - - if (!uploadID) { - throw new Error('[s3] failed to initiate multipart upload: no UploadId received'); - } - - const uploadedParts: { PartNumber: number; ETag: string }[] = []; - let partNumber = 1; - const chunks: Buffer[] = []; - let currentSize = 0; - - try { - for await (const chunk of stream) { - console.log(`[s3] received chunk of size ${(chunk.length / (1024 * 1024)).toFixed(2)}MB for ${filename}`); - - chunks.push(chunk); - currentSize += chunk.length; - - while (currentSize >= chunkSize) { - const partData = Buffer.allocUnsafe(chunkSize); - let copied = 0; - - while (copied < chunkSize && chunks.length > 0) { - const currentChunk = chunks[0]; - const needed = chunkSize - copied; - const available = currentChunk.length; - - if (available <= needed) { - currentChunk.copy(partData, copied); - copied += available; - chunks.shift(); - } else { - currentChunk.copy(partData, copied, 0, needed); - copied += needed; - chunks[0] = currentChunk.subarray(needed); - } - } - - currentSize -= chunkSize; - - console.log( - `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, - ); - console.log( - `[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`, - ); - - stream.pause(); - - const uploadPartResult = await this.client.send( - new UploadPartCommand({ - Bucket: this.bucket, - Key: remotePath, - UploadId: uploadID, - PartNumber: partNumber, - Body: partData, - }), - ); - - // explicitly clear the part data to help GC - partData.fill(0); - - console.log(`[s3] uploaded part ${partNumber}, resume reading`); - stream.resume(); - - if (!uploadPartResult.ETag) { - throw new Error(`[s3] failed to upload part ${partNumber}: no ETag received`); - } - - uploadedParts.push({ - PartNumber: partNumber, - ETag: uploadPartResult.ETag, - }); - - partNumber++; - } - } - - if (currentSize > 0) { - console.log(`[s3] uploading final part ${partNumber} [${bytesToString(currentSize)}] for ${filename}`); - - const finalPart = Buffer.allocUnsafe(currentSize); - let offset = 0; - - for (const chunk of chunks) { - chunk.copy(finalPart, offset); - offset += chunk.length; - } - - const uploadPartResult = await this.client.send( - new UploadPartCommand({ - Bucket: this.bucket, - Key: remotePath, - UploadId: uploadID, - PartNumber: partNumber, - Body: finalPart, - }), - ); - - // explicitly clear buffer references - chunks.length = 0; - finalPart.fill(0); - - if (!uploadPartResult.ETag) { - throw new Error(`[s3] failed to upload final part ${partNumber}: no ETag received`); - } - - uploadedParts.push({ - PartNumber: partNumber, - ETag: uploadPartResult.ETag, - }); - } - - console.log(`[s3] completing multipart upload for ${filename} with ${uploadedParts.length} parts`); - - await this.client.send( - new CompleteMultipartUploadCommand({ - Bucket: this.bucket, - Key: remotePath, - UploadId: uploadID, - MultipartUpload: { - Parts: uploadedParts, - }, - }), - ); - - console.log(`[s3] multipart upload completed successfully for ${filename}`); - } catch (error) { - console.error(`[s3] multipart upload failed, aborting: ${(error as Error).message}`); - - await this.client.send( - new AbortMultipartUploadCommand({ - Bucket: this.bucket, - Key: remotePath, - UploadId: uploadID, - }), - ); - - throw error; - } - } - - async saveResultDetails(resultID: string, resultDetails: ResultDetails, size: number): Promise { - const metaData = { - resultID, - createdAt: new Date().toISOString(), - project: resultDetails?.project ?? '', - ...resultDetails, - size: bytesToString(size), - sizeBytes: size, - }; - - await this.write(RESULTS_BUCKET, [ - { - name: `${resultID}.json`, - content: JSON.stringify(metaData), - }, - ]); - - return metaData as Result; - } - - private async uploadReport(reportId: string, reportPath: string, remotePath: string) { - console.log(`[s3] upload report: ${reportPath}`); - - const files = await fs.readdir(reportPath, { recursive: true, withFileTypes: true }); - - await processBatch(this, files, this.batchSize, async (file) => { - if (!file.isFile()) { - return; - } - - console.log(`[s3] uploading file: ${JSON.stringify(file)}`); - - const nestedPath = (file as any).path.split(reportId).pop(); - const s3Path = path.join(remotePath, nestedPath ?? '', file.name); - - console.log(`[s3] uploading to ${s3Path}`); - - const { error } = await withError(this.uploadFileWithRetry(s3Path, path.join((file as any).path, file.name))); - - if (error) { - console.error(`[s3] failed to upload report: ${error.message}`); - throw new Error(`[s3] failed to upload report: ${error.message}`); - } - }); - } - - private async uploadFileWithRetry(remotePath: string, filePath: string, attempt = 1): Promise { - if (attempt > 3) { - throw new Error(`[s3] failed to upload file after ${attempt} attempts: ${filePath}`); - } - - const fileStream = createReadStream(filePath); - - const { error } = await withError( - this.client.send( - new PutObjectCommand({ - Bucket: this.bucket, - Key: remotePath, - Body: fileStream, - }), - ), - ); - - if (error) { - console.error(`[s3] failed to upload file: ${error.message}`); - console.log(`[s3] will retry in 3s...`); - - return await this.uploadFileWithRetry(remotePath, filePath, attempt + 1); - } - } - - private async clearTempFolders(id?: string) { - const withReportPathMaybe = id ? ` for report ${id}` : ''; - - console.log(`[s3] clear temp folders${withReportPathMaybe}`); - - await withError(fs.rm(path.join(TMP_FOLDER, id ?? ''), { recursive: true, force: true })); - await withError(fs.rm(REPORTS_FOLDER, { recursive: true, force: true })); - } - - async generateReport(resultsIds: string[], metadata?: ReportMetadata): Promise { - console.log(`[s3] generate report from results: ${JSON.stringify(resultsIds)}`); - console.log(`[s3] create temp folders`); - const { error: mkdirReportsError } = await withError(fs.mkdir(REPORTS_FOLDER, { recursive: true })); - - if (mkdirReportsError) { - console.error(`[s3] failed to create reports folder: ${mkdirReportsError.message}`); - } - - const reportId = randomUUID(); - const tempFolder = path.join(TMP_FOLDER, reportId); - - const { error: mkdirTempError } = await withError(fs.mkdir(tempFolder, { recursive: true })); - - if (mkdirTempError) { - console.error(`[s3] failed to create temporary folder: ${mkdirTempError.message}`); - } - - console.log(`[s3] start processing...`); - - let continuationToken: string | undefined; - - do { - const response = await this.client.send( - new ListObjectsV2Command({ - Bucket: this.bucket, - Prefix: RESULTS_BUCKET, - ContinuationToken: continuationToken, - }), - ); - - for (const result of response.Contents ?? []) { - if (!result.Key) continue; - - const fileName = path.basename(result.Key); - - const id = fileName.replace(path.extname(fileName), ''); - - if (resultsIds.includes(id) && path.extname(fileName) === '.zip') { - console.log(`[s3] file id is in target results, downloading...`); - const localFilePath = path.join(tempFolder, fileName); - - const { error } = await withError( - (async () => { - const response = await this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: result.Key!, - }), - ); - - const stream = response.Body as Readable; - const writeStream = createWriteStream(localFilePath); - - return new Promise((resolve, reject) => { - stream.pipe(writeStream); - writeStream.on('finish', resolve); - writeStream.on('error', reject); - stream.on('error', reject); - }); - })(), - ); - - if (error) { - console.error(`[s3] failed to download ${result.Key}: ${error.message}`); - - throw new Error(`failed to download ${result.Key}: ${error.message}`); - } - - console.log(`[s3] Downloaded: ${result.Key} to ${localFilePath}`); - } - } - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - } while (continuationToken); - - const { reportPath } = await generatePlaywrightReport(reportId, metadata!); - - console.log(`[s3] report generated: ${reportId} | ${reportPath}`); - - const { result: info, error: parseReportMetadataError } = await withError( - this.parseReportMetadata(reportId, reportPath, metadata), - ); - - if (parseReportMetadataError) console.error(parseReportMetadataError.message); - - const remotePath = path.join(REPORTS_BUCKET, metadata?.project ?? '', reportId); - - const { error: uploadError } = await withError(this.uploadReport(reportId, reportPath, remotePath)); - - if (uploadError) { - console.error(`[s3] failed to upload report: ${uploadError.message}`); - } else { - const { error } = await withError(this.saveReportMetadata(reportId, reportPath, info ?? metadata ?? {})); - - if (error) console.error(`[s3] failed to save report metadata: ${error.message}`); - } - - await this.clearTempFolders(reportId); - - return reportId; - } - - private async saveReportMetadata(reportId: string, reportPath: string, metadata: ReportMetadata) { - console.log(`[s3] report uploaded: ${reportId}, uploading metadata to ${reportPath}`); - const { error: metadataError } = await withError( - this.write(path.join(REPORTS_BUCKET, metadata.project ?? '', reportId), [ - { - name: REPORT_METADATA_FILE, - content: JSON.stringify(metadata), - }, - ]), - ); - - if (metadataError) console.error(`[s3] failed to upload report metadata: ${metadataError.message}`); - } - - private async parseReportMetadata( - reportId: string, - reportPath: string, - metadata?: Record, - htmlContent?: string, // to pass file content if stored on s3 - ): Promise { - console.log(`[s3] creating report metadata for ${reportId} and ${reportPath}`); - const html = htmlContent ?? (await fs.readFile(path.join(reportPath, 'index.html'), 'utf-8')); - - const info = await parse(html as string); - - const content = Object.assign(info, metadata, { - reportId, - createdAt: new Date().toISOString(), - }); - - return content; - } - - async readConfigFile(): Promise<{ result?: SiteWhiteLabelConfig; error: Error | null }> { - await this.ensureBucketExist(); - console.log(`[s3] checking config file`); - - const { result: response, error } = await withError( - this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: APP_CONFIG_S3, - }), - ), - ); - - if (error) { - console.error(`[s3] failed to read config file: ${error.message}`); - - return { error }; - } - - const stream = response?.Body as Readable; - let existingConfig = ''; - - for await (const chunk of stream ?? []) { - existingConfig += chunk.toString(); - } - - try { - const parsed = JSON.parse(existingConfig); - - const isValid = isConfigValid(parsed); - - if (!isValid) { - return { error: new Error('invalid config') }; - } - - // ensure custom images available locally in data folder - for (const image of [ - { path: parsed.faviconPath, default: defaultConfig.faviconPath }, - { path: parsed.logoPath, default: defaultConfig.logoPath }, - ]) { - if (!image) continue; - if (image.path === image.default) continue; - - const localPath = path.join(DATA_FOLDER, image.path); - const { error: accessError } = await withError(fs.access(localPath)); - - if (accessError) { - const remotePath = path.join(DATA_PATH, image.path); - - console.log(`[s3] downloading config image: ${remotePath} to ${localPath}`); - - const response = await this.client.send( - new GetObjectCommand({ - Bucket: this.bucket, - Key: remotePath, - }), - ); - - const stream = response.Body as Readable; - const writeStream = createWriteStream(localPath); - - await new Promise((resolve, reject) => { - stream.pipe(writeStream); - writeStream.on('finish', resolve); - writeStream.on('error', reject); - stream.on('error', reject); - }); - } - } - - return { result: parsed, error: null }; - } catch (e) { - return { error: new Error(`failed to parse config: ${e instanceof Error ? e.message : e}`) }; - } - } - - async saveConfigFile(config: Partial) { - console.log(`[s3] writing config file`); - - const { result: existingConfig, error: readExistingConfigError } = await this.readConfigFile(); - - if (readExistingConfigError) { - console.error(`[s3] failed to read existing config file: ${readExistingConfigError.message}`); - } - - const { error: clearExistingConfigError } = await withError(this.clear(APP_CONFIG_S3)); - - if (clearExistingConfigError) { - console.error(`[s3] failed to clear existing config file: ${clearExistingConfigError.message}`); - } - - const uploadConfig = { ...(existingConfig ?? {}), ...config } as SiteWhiteLabelConfig; - - const isDefaultImage = (key: keyof SiteWhiteLabelConfig) => config[key] && config[key] === defaultConfig[key]; - - const shouldBeUploaded = async (key: keyof SiteWhiteLabelConfig) => { - if (!config[key]) return false; - if (isDefaultImage(key)) return false; - - const imagePath = key === 'logoPath' ? uploadConfig.logoPath : uploadConfig.faviconPath; - - const { result } = await withError( - this.client.send( - new HeadObjectCommand({ - Bucket: this.bucket, - Key: path.join(DATA_PATH, imagePath), - }), - ), - ); - - if (!result) { - return true; - } - - return false; - }; - - if (await shouldBeUploaded('logoPath')) { - await this.uploadConfigImage(uploadConfig.logoPath); - } - - if (await shouldBeUploaded('faviconPath')) { - await this.uploadConfigImage(uploadConfig.faviconPath); - } - - const { error } = await withError( - this.write(DATA_PATH, [ - { - name: 'config.json', - content: JSON.stringify(uploadConfig, null, 2), - }, - ]), - ); - - if (error) console.error(`[s3] failed to write config file: ${error.message}`); - - return { result: uploadConfig, error }; - } - - private async uploadConfigImage(imagePath: string): Promise { - console.log(`[s3] uploading config image: ${imagePath}`); - - const localPath = path.join(DATA_FOLDER, imagePath); - const remotePath = path.join(DATA_PATH, imagePath); - - const { error } = await withError(this.uploadFileWithRetry(remotePath, localPath)); - - if (error) { - console.error(`[s3] failed to upload config image: ${error.message}`); - - return error; - } - - return null; - } + private static instance: S3; + private readonly client: S3Client; + private readonly bucket: string; + private readonly batchSize: number; + + private constructor() { + this.client = createClient(); + this.bucket = env.S3_BUCKET; + this.batchSize = env.S3_BATCH_SIZE; + } + + public static getInstance() { + if (!S3.instance) { + S3.instance = new S3(); + } + + return S3.instance; + } + + private async ensureBucketExist() { + const { error } = await withError( + this.client.send(new HeadBucketCommand({ Bucket: this.bucket })), + ); + + if (!error) { + return; + } + + if (error.name === "NotFound") { + console.log(`[s3] bucket ${this.bucket} does not exist, creating...`); + + const { error } = await withError( + this.client.send( + new CreateBucketCommand({ + Bucket: this.bucket, + }), + ), + ); + + if (error) { + console.error("[s3] failed to create bucket:", error); + } + } + + console.error("[s3] failed to check that bucket exist:", error); + } + + private async write( + dir: string, + files: { + name: string; + content: Readable | Buffer | string; + size?: number; + }[], + ) { + await this.ensureBucketExist(); + for (const file of files) { + const filePath = path.join(dir, file.name); + + console.log(`[s3] writing ${filePath}`); + + const content = + typeof file.content === "string" + ? Buffer.from(file.content) + : file.content; + + await this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: path.normalize(filePath), + Body: content, + }), + ); + } + } + + private async read(targetPath: string, contentType?: string | null) { + await this.ensureBucketExist(); + console.log(`[s3] read ${targetPath}`); + + const remotePath = targetPath.includes(REPORTS_BUCKET) + ? targetPath + : `${REPORTS_BUCKET}/${targetPath}`; + + console.log(`[s3] reading from remote path: ${remotePath}`); + + const { result: response, error } = await withError( + this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ), + ); + + if (error ?? !response?.Body) { + return { result: null, error }; + } + + const stream = response.Body as Readable; + + const readStream = new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on("data", (chunk: Uint8Array) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + const fullContent = Buffer.concat(chunks); + + resolve(fullContent); + }); + + stream.on("error", (error) => { + console.error(`[s3] failed to read stream: ${error.message}`); + reject(error); + }); + }); + + const { result, error: readError } = await withError(readStream); + + return { + result: contentType === "text/html" ? result?.toString("utf-8") : result, + error: error ?? readError ?? null, + }; + } + + async clear(...path: string[]) { + console.log(`[s3] clearing ${path}`); + // avoid using "removeObjects" as it is not supported by every S3-compatible provider + // for example, Google Cloud Storage. + await processBatch( + this, + path, + this.batchSize, + async (object) => { + await this.client.send( + new DeleteObjectCommand({ + Bucket: this.bucket, + Key: object, + }), + ); + }, + ); + } + + async getFolderSize( + folderPath: string, + ): Promise<{ size: number; resultCount: number; indexCount: number }> { + let resultCount = 0; + let indexCount = 0; + let totalSize = 0; + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: folderPath, + ContinuationToken: continuationToken, + }), + ); + + for (const obj of response.Contents ?? []) { + if (obj.Key?.endsWith(".zip")) { + resultCount += 1; + } + + if ( + obj.Key?.endsWith("index.html") && + !obj.Key.includes("/trace/index.html") + ) { + indexCount += 1; + } + + totalSize += obj?.Size ?? 0; + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + return { size: totalSize, resultCount, indexCount }; + } + + async getServerDataInfo(): Promise { + await this.ensureBucketExist(); + console.log("[s3] getting server data"); + + const [results, reports] = await Promise.all([ + this.getFolderSize(RESULTS_BUCKET), + this.getFolderSize(REPORTS_BUCKET), + ]); + + const dataSize = results.size + reports.size; + + return { + dataFolderSizeinMB: bytesToString(dataSize), + numOfResults: results.resultCount, + resultsFolderSizeinMB: bytesToString(results.size), + numOfReports: reports.indexCount, + reportsFolderSizeinMB: bytesToString(reports.size), + }; + } + + async readFile( + targetPath: string, + contentType: string | null, + ): Promise { + console.log(`[s3] reading ${targetPath} | ${contentType}`); + const { result, error } = await this.read(targetPath, contentType); + + if (error) { + console.error(`[s3] failed to read file ${targetPath}: ${error.message}`); + throw new Error(`[s3] failed to read file: ${error.message}`); + } + + return result!; + } + + async readResults(): Promise { + await this.ensureBucketExist(); + + console.log("[s3] reading results"); + + const jsonFiles: _Object[] = []; + const resultSizes = new Map(); + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: RESULTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + if (file.Key.endsWith(".zip")) { + const resultID = path.basename(file.Key, ".zip"); + + resultSizes.set(resultID, file.Size ?? 0); + } + + if (file.Key.endsWith(".json")) { + jsonFiles.push(file); + } + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + console.log(`[s3] found ${jsonFiles.length} json files`); + + if (!jsonFiles) { + return { + results: [], + total: 0, + }; + } + + const results = await processBatch<_Object, Result>( + this, + jsonFiles, + this.batchSize, + async (file) => { + console.log(`[s3.batch] reading result: ${JSON.stringify(file)}`); + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: file.Key!, + }), + ); + + const stream = response.Body as Readable; + let jsonString = ""; + + for await (const chunk of stream) { + jsonString += chunk.toString(); + } + + const parsed = JSON.parse(jsonString); + + return parsed; + }, + ); + + return { + results: results.map((result) => { + const sizeBytes = resultSizes.get(result.resultID) ?? 0; + + return { + ...result, + sizeBytes, + size: result.size ?? bytesToString(sizeBytes), + }; + }) as Result[], + total: results.length, + }; + } + + async readReport( + reportID: string, + reportPath: string, + ): Promise { + await this.ensureBucketExist(); + + console.log(`[s3] reading report ${reportID} metadata`); + + const relativePath = path.relative(reportPath, REPORTS_BUCKET); + + const objectKey = path.join( + REPORTS_BUCKET, + relativePath, + REPORT_METADATA_FILE, + ); + + console.log(`[s3] checking existence of result: ${objectKey}`); + const { error: headError } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ), + ); + + if (headError) { + throw new Error(`failed to check ${objectKey}: ${headError.message}`); + } + + console.log(`[s3] downloading metadata file: ${objectKey}`); + const localFilePath = path.join(TMP_FOLDER, reportID, REPORT_METADATA_FILE); + + const { error: downloadError } = await withError( + (async () => { + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localFilePath); + + return new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + })(), + ); + + if (downloadError) { + console.error( + `[s3] failed to download ${objectKey}: ${downloadError.message}`, + ); + + throw new Error( + `failed to download ${objectKey}: ${downloadError.message}`, + ); + } + + try { + const content = await fs.readFile(localFilePath, "utf-8"); + + const metadata = JSON.parse(content); + + return isReportHistory(metadata) ? metadata : null; + } catch (e) { + console.error( + `[s3] failed to read or parse metadata file: ${(e as Error).message}`, + ); + + return null; + } + } + + async readReports(): Promise { + await this.ensureBucketExist(); + + console.log(`[s3] reading reports from external storage`); + + const reports: Report[] = []; + const reportSizes = new Map(); + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: REPORTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + const reportID = getFileReportID(file.Key); + + const newSize = (reportSizes.get(reportID) ?? 0) + (file.Size ?? 0); + + reportSizes.set(reportID, newSize); + + if (!file.Key.endsWith("index.html") || file.Key.includes("trace")) { + continue; + } + + const dir = path.dirname(file.Key); + const id = path.basename(dir); + const parentDir = path.basename(path.dirname(dir)); + + const projectName = parentDir === REPORTS_PATH ? "" : parentDir; + + const report = { + reportID: id, + project: projectName, + createdAt: file.LastModified ?? new Date(), + reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ""}/${id}/index.html`, + size: "", + sizeBytes: 0, + }; + + reports.push(report); + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + const withMetadata = await this.getReportsMetadata( + reports as ReportHistory[], + ); + + return { + reports: withMetadata.map((report) => { + const sizeBytes = reportSizes.get(report.reportID) ?? 0; + + return { + ...report, + sizeBytes, + size: bytesToString(sizeBytes), + }; + }), + total: withMetadata.length, + }; + } + + async getReportsMetadata(reports: ReportHistory[]): Promise { + return await processBatch( + this, + reports, + this.batchSize, + async (report) => { + console.log(`[s3.batch] reading report ${report.reportID} metadata`); + + const { result: metadata, error: metadataError } = await withError( + this.readOrParseReportMetadata(report.reportID, report.project), + ); + + if (metadataError) { + console.error( + `[s3] failed to read or create metadata for ${report.reportID}: ${metadataError.message}`, + ); + + return report; + } + + if (!metadata) { + return report; + } + + return Object.assign(metadata, report); + }, + ); + } + + async readOrParseReportMetadata( + id: string, + projectName: string, + ): Promise { + const { result: metadataContent, error: metadataError } = await withError( + this.readFile( + path.join(REPORTS_BUCKET, projectName, id, REPORT_METADATA_FILE), + "utf-8", + ), + ); + + if (metadataError) + console.error( + `[s3] failed to read metadata for ${id}: ${metadataError.message}`, + ); + + const metadata = + metadataContent && !metadataError + ? JSON.parse(metadataContent.toString()) + : {}; + + if (isReportHistory(metadata)) { + console.log(`metadata found for report ${id}`); + + return metadata; + } + + console.log(`metadata file not found for ${id}, creating new metadata`); + try { + const { result: htmlContent, error: htmlError } = await withError( + this.readFile( + path.join(REPORTS_BUCKET, projectName, id, "index.html"), + "utf-8", + ), + ); + + if (htmlError) + console.error( + `[s3] failed to read index.html for ${id}: ${htmlError.message}`, + ); + + const created = await this.parseReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + { + project: projectName, + reportID: id, + }, + htmlContent?.toString(), + ); + + console.log( + `metadata object created for ${id}: ${JSON.stringify(created)}`, + ); + + await this.saveReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + created, + ); + + Object.assign(metadata, created); + } catch (e) { + console.error( + `failed to create metadata for ${id}: ${(e as Error).message}`, + ); + } + + return metadata; + } + + async deleteResults(resultIDs: string[]): Promise { + const objects = resultIDs.flatMap((id) => [ + `${RESULTS_BUCKET}/${id}.json`, + `${RESULTS_BUCKET}/${id}.zip`, + ]); + + await withError(this.clear(...objects)); + } + + private async getReportObjects(reportsIDs: string[]): Promise { + const files: string[] = []; + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: REPORTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + const reportID = path.basename(path.dirname(file.Key)); + + if (reportsIDs.includes(reportID)) { + files.push(file.Key); + } + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + return files; + } + + async deleteReports(reports: ReportPath[]): Promise { + const ids = reports.map((r) => r.reportID); + const objects = await this.getReportObjects(ids); + + await withError(this.clear(...objects)); + } + + async generatePresignedUploadUrl(fileName: string) { + await this.ensureBucketExist(); + const objectKey = path.join(RESULTS_BUCKET, fileName); + const expiry = 30 * 60; // 30 minutes + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }); + + return await getSignedUrl(this.client, command, { expiresIn: expiry }); + } + + async saveResult(filename: string, stream: PassThrough) { + await this.ensureBucketExist(); + + const chunkSizeMB = env.S3_MULTIPART_CHUNK_SIZE_MB; + const chunkSize = chunkSizeMB * 1024 * 1024; // bytes + + console.log( + `[s3] starting multipart upload for ${filename} with chunk size ${chunkSizeMB}MB`, + ); + + const remotePath = path.join(RESULTS_BUCKET, filename); + + const { UploadId: uploadID } = await this.client.send( + new CreateMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ); + + if (!uploadID) { + throw new Error( + "[s3] failed to initiate multipart upload: no UploadId received", + ); + } + + const uploadedParts: { PartNumber: number; ETag: string }[] = []; + let partNumber = 1; + const chunks: Buffer[] = []; + let currentSize = 0; + + try { + for await (const chunk of stream) { + console.log( + `[s3] received chunk of size ${(chunk.length / (1024 * 1024)).toFixed(2)}MB for ${filename}`, + ); + + chunks.push(chunk); + currentSize += chunk.length; + + while (currentSize >= chunkSize) { + const partData = Buffer.allocUnsafe(chunkSize); + let copied = 0; + + while (copied < chunkSize && chunks.length > 0) { + const currentChunk = chunks[0]; + const needed = chunkSize - copied; + const available = currentChunk.length; + + if (available <= needed) { + currentChunk.copy(partData, copied); + copied += available; + chunks.shift(); + } else { + currentChunk.copy(partData, copied, 0, needed); + copied += needed; + chunks[0] = currentChunk.subarray(needed); + } + } + + currentSize -= chunkSize; + + console.log( + `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, + ); + console.log( + `[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`, + ); + + stream.pause(); + + const uploadPartResult = await this.client.send( + new UploadPartCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + PartNumber: partNumber, + Body: partData, + }), + ); + + // explicitly clear the part data to help GC + partData.fill(0); + + console.log(`[s3] uploaded part ${partNumber}, resume reading`); + stream.resume(); + + if (!uploadPartResult.ETag) { + throw new Error( + `[s3] failed to upload part ${partNumber}: no ETag received`, + ); + } + + uploadedParts.push({ + PartNumber: partNumber, + ETag: uploadPartResult.ETag, + }); + + partNumber++; + } + } + + if (currentSize > 0) { + console.log( + `[s3] uploading final part ${partNumber} [${bytesToString(currentSize)}] for ${filename}`, + ); + + const finalPart = Buffer.allocUnsafe(currentSize); + let offset = 0; + + for (const chunk of chunks) { + chunk.copy(finalPart, offset); + offset += chunk.length; + } + + const uploadPartResult = await this.client.send( + new UploadPartCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + PartNumber: partNumber, + Body: finalPart, + }), + ); + + // explicitly clear buffer references + chunks.length = 0; + finalPart.fill(0); + + if (!uploadPartResult.ETag) { + throw new Error( + `[s3] failed to upload final part ${partNumber}: no ETag received`, + ); + } + + uploadedParts.push({ + PartNumber: partNumber, + ETag: uploadPartResult.ETag, + }); + } + + console.log( + `[s3] completing multipart upload for ${filename} with ${uploadedParts.length} parts`, + ); + + await this.client.send( + new CompleteMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + MultipartUpload: { + Parts: uploadedParts, + }, + }), + ); + + console.log( + `[s3] multipart upload completed successfully for ${filename}`, + ); + } catch (error) { + console.error( + `[s3] multipart upload failed, aborting: ${(error as Error).message}`, + ); + + await this.client.send( + new AbortMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + }), + ); + + throw error; + } + } + + async saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, + ): Promise { + const metaData = { + resultID, + createdAt: new Date().toISOString(), + project: resultDetails?.project ?? "", + ...resultDetails, + size: bytesToString(size), + sizeBytes: size, + }; + + await this.write(RESULTS_BUCKET, [ + { + name: `${resultID}.json`, + content: JSON.stringify(metaData), + }, + ]); + + return metaData as Result; + } + + private async uploadReport( + reportId: string, + reportPath: string, + remotePath: string, + ) { + console.log(`[s3] upload report: ${reportPath}`); + + const files = await fs.readdir(reportPath, { + recursive: true, + withFileTypes: true, + }); + + await processBatch(this, files, this.batchSize, async (file) => { + if (!file.isFile()) { + return; + } + + console.log(`[s3] uploading file: ${JSON.stringify(file)}`); + + const nestedPath = (file as any).path.split(reportId).pop(); + const s3Path = path.join(remotePath, nestedPath ?? "", file.name); + + console.log(`[s3] uploading to ${s3Path}`); + + const { error } = await withError( + this.uploadFileWithRetry( + s3Path, + path.join((file as any).path, file.name), + ), + ); + + if (error) { + console.error(`[s3] failed to upload report: ${error.message}`); + throw new Error(`[s3] failed to upload report: ${error.message}`); + } + }); + } + + private async uploadFileWithRetry( + remotePath: string, + filePath: string, + attempt = 1, + ): Promise { + if (attempt > 3) { + throw new Error( + `[s3] failed to upload file after ${attempt} attempts: ${filePath}`, + ); + } + + const fileStream = createReadStream(filePath); + + const { error } = await withError( + this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + Body: fileStream, + }), + ), + ); + + if (error) { + console.error(`[s3] failed to upload file: ${error.message}`); + console.log(`[s3] will retry in 3s...`); + + return await this.uploadFileWithRetry(remotePath, filePath, attempt + 1); + } + } + + private async clearTempFolders(id?: string) { + const withReportPathMaybe = id ? ` for report ${id}` : ""; + + console.log(`[s3] clear temp folders${withReportPathMaybe}`); + + await withError( + fs.rm(path.join(TMP_FOLDER, id ?? ""), { recursive: true, force: true }), + ); + await withError(fs.rm(REPORTS_FOLDER, { recursive: true, force: true })); + } + + async generateReport( + resultsIds: string[], + metadata?: ReportMetadata, + ): Promise<{ reportId: UUID; reportPath: string }> { + console.log( + `[s3] generate report from results: ${JSON.stringify(resultsIds)}`, + ); + console.log(`[s3] create temp folders`); + const { error: mkdirReportsError } = await withError( + fs.mkdir(REPORTS_FOLDER, { recursive: true }), + ); + + if (mkdirReportsError) { + console.error( + `[s3] failed to create reports folder: ${mkdirReportsError.message}`, + ); + } + + const reportId = randomUUID(); + const tempFolder = path.join(TMP_FOLDER, reportId); + + const { error: mkdirTempError } = await withError( + fs.mkdir(tempFolder, { recursive: true }), + ); + + if (mkdirTempError) { + console.error( + `[s3] failed to create temporary folder: ${mkdirTempError.message}`, + ); + } + + console.log(`[s3] start processing...`); + + for (const resultId of resultsIds) { + const fileName = `${resultId}.zip`; + const objectKey = path.join(RESULTS_BUCKET, fileName); + + console.log(`[s3] checking existence of result: ${objectKey}`); + const { error: headError } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ), + ); + + if (headError) { + console.error( + `[s3] result ${resultId} not found, skipping: ${headError.message}`, + ); + throw new Error(`failed to check ${objectKey}: ${headError.message}`); + } + + console.log(`[s3] downloading result: ${objectKey}`); + const localFilePath = path.join(tempFolder, fileName); + + const { error: downloadError } = await withError( + (async () => { + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localFilePath); + + return new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + })(), + ); + + if (downloadError) { + console.error( + `[s3] failed to download ${objectKey}: ${downloadError.message}`, + ); + + throw new Error( + `failed to download ${objectKey}: ${downloadError.message}`, + ); + } + + console.log(`[s3] downloaded: ${objectKey} to ${localFilePath}`); + } + + const { reportPath } = await generatePlaywrightReport(reportId, metadata!); + + console.log(`[s3] report generated: ${reportId} | ${reportPath}`); + + const { result: info, error: parseReportMetadataError } = await withError( + this.parseReportMetadata(reportId, reportPath, metadata), + ); + + if (parseReportMetadataError) + console.error(parseReportMetadataError.message); + + const remotePath = path.join( + REPORTS_BUCKET, + metadata?.project ?? "", + reportId, + ); + + const { error: uploadError } = await withError( + this.uploadReport(reportId, reportPath, remotePath), + ); + + if (uploadError) { + console.error(`[s3] failed to upload report: ${uploadError.message}`); + } else { + const { error } = await withError( + this.saveReportMetadata(reportId, reportPath, info ?? metadata ?? {}), + ); + + if (error) + console.error(`[s3] failed to save report metadata: ${error.message}`); + } + + await this.clearTempFolders(reportId); + + return { reportId, reportPath }; + } + + private async saveReportMetadata( + reportId: string, + reportPath: string, + metadata: ReportMetadata, + ) { + console.log( + `[s3] report uploaded: ${reportId}, uploading metadata to ${reportPath}`, + ); + const { error: metadataError } = await withError( + this.write(path.join(REPORTS_BUCKET, metadata.project ?? "", reportId), [ + { + name: REPORT_METADATA_FILE, + content: JSON.stringify(metadata), + }, + ]), + ); + + if (metadataError) + console.error( + `[s3] failed to upload report metadata: ${metadataError.message}`, + ); + } + + private async parseReportMetadata( + reportId: string, + reportPath: string, + metadata?: Record, + htmlContent?: string, // to pass file content if stored on s3 + ): Promise { + console.log( + `[s3] creating report metadata for ${reportId} and ${reportPath}`, + ); + const html = + htmlContent ?? + (await fs.readFile(path.join(reportPath, "index.html"), "utf-8")); + + const info = await parse(html as string); + + const content = Object.assign(info, metadata, { + reportId, + createdAt: new Date().toISOString(), + }); + + return content; + } + + async readConfigFile(): Promise<{ + result?: SiteWhiteLabelConfig; + error: Error | null; + }> { + await this.ensureBucketExist(); + console.log(`[s3] checking config file`); + + const { result: response, error } = await withError( + this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: APP_CONFIG_S3, + }), + ), + ); + + if (error) { + console.error(`[s3] failed to read config file: ${error.message}`); + + return { error }; + } + + const stream = response?.Body as Readable; + let existingConfig = ""; + + for await (const chunk of stream ?? []) { + existingConfig += chunk.toString(); + } + + try { + const parsed = JSON.parse(existingConfig); + + const isValid = isConfigValid(parsed); + + if (!isValid) { + return { error: new Error("invalid config") }; + } + + // ensure custom images available locally in data folder + for (const image of [ + { path: parsed.faviconPath, default: defaultConfig.faviconPath }, + { path: parsed.logoPath, default: defaultConfig.logoPath }, + ]) { + if (!image) continue; + if (image.path === image.default) continue; + + const localPath = path.join(DATA_FOLDER, image.path); + const { error: accessError } = await withError(fs.access(localPath)); + + if (accessError) { + const remotePath = path.join(DATA_PATH, image.path); + + console.log( + `[s3] downloading config image: ${remotePath} to ${localPath}`, + ); + + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localPath); + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + } + } + + return { result: parsed, error: null }; + } catch (e) { + return { + error: new Error( + `failed to parse config: ${e instanceof Error ? e.message : e}`, + ), + }; + } + } + + async saveConfigFile(config: Partial) { + console.log(`[s3] writing config file`); + + const { result: existingConfig, error: readExistingConfigError } = + await this.readConfigFile(); + + if (readExistingConfigError) { + console.error( + `[s3] failed to read existing config file: ${readExistingConfigError.message}`, + ); + } + + const { error: clearExistingConfigError } = await withError( + this.clear(APP_CONFIG_S3), + ); + + if (clearExistingConfigError) { + console.error( + `[s3] failed to clear existing config file: ${clearExistingConfigError.message}`, + ); + } + + const uploadConfig = { + ...(existingConfig ?? {}), + ...config, + } as SiteWhiteLabelConfig; + + const isDefaultImage = (key: keyof SiteWhiteLabelConfig) => + config[key] && config[key] === defaultConfig[key]; + + const shouldBeUploaded = async (key: keyof SiteWhiteLabelConfig) => { + if (!config[key]) return false; + if (isDefaultImage(key)) return false; + + const imagePath = + key === "logoPath" ? uploadConfig.logoPath : uploadConfig.faviconPath; + + const { result } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: path.join(DATA_PATH, imagePath), + }), + ), + ); + + if (!result) { + return true; + } + + return false; + }; + + if (await shouldBeUploaded("logoPath")) { + await this.uploadConfigImage(uploadConfig.logoPath); + } + + if (await shouldBeUploaded("faviconPath")) { + await this.uploadConfigImage(uploadConfig.faviconPath); + } + + const { error } = await withError( + this.write(DATA_PATH, [ + { + name: "config.json", + content: JSON.stringify(uploadConfig, null, 2), + }, + ]), + ); + + if (error) + console.error(`[s3] failed to write config file: ${error.message}`); + + return { result: uploadConfig, error }; + } + + private async uploadConfigImage(imagePath: string): Promise { + console.log(`[s3] uploading config image: ${imagePath}`); + + const localPath = path.join(DATA_FOLDER, imagePath); + const remotePath = path.join(DATA_PATH, imagePath); + + const { error } = await withError( + this.uploadFileWithRetry(remotePath, localPath), + ); + + if (error) { + console.error(`[s3] failed to upload config image: ${error.message}`); + + return error; + } + + return null; + } } diff --git a/app/lib/storage/types.ts b/app/lib/storage/types.ts index 69b56e77..1c243b43 100644 --- a/app/lib/storage/types.ts +++ b/app/lib/storage/types.ts @@ -1,99 +1,121 @@ -import { PassThrough } from 'node:stream'; +import type { PassThrough } from "node:stream"; +import type { ReportInfo, ReportTest } from "@/app/lib/parser/types"; -import { type Pagination } from './pagination'; - -import { type SiteWhiteLabelConfig, type UUID } from '@/app/types'; -import { type ReportInfo, type ReportTest } from '@/app/lib/parser/types'; +import type { SiteWhiteLabelConfig, UUID } from "@/app/types"; +import type { Pagination } from "./pagination"; export interface Storage { - getServerDataInfo: () => Promise; - readFile: (targetPath: string, contentType: string | null) => Promise; - readResults: () => Promise; - readReports: () => Promise; - deleteResults: (resultIDs: string[]) => Promise; - deleteReports: (reports: ReportPath[]) => Promise; - saveResult: (filename: string, stream: PassThrough) => Promise; - saveResultDetails: (resultID: string, resultDetails: ResultDetails, size: number) => Promise; - generateReport: (resultsIds: string[], metadata?: ReportMetadata) => Promise; - readConfigFile: () => Promise<{ result?: SiteWhiteLabelConfig; error: Error | null }>; - saveConfigFile: ( - config: Partial, - ) => Promise<{ result: SiteWhiteLabelConfig; error: Error | null }>; + getServerDataInfo: () => Promise; + readFile: ( + targetPath: string, + contentType: string | null, + ) => Promise; + readResults: () => Promise; + readReports: () => Promise; + readReport: ( + reportID: string, + reportPath: string, + ) => Promise; + deleteResults: (resultIDs: string[]) => Promise; + deleteReports: (reports: ReportPath[]) => Promise; + saveResult: (filename: string, stream: PassThrough) => Promise; + saveResultDetails: ( + resultID: string, + resultDetails: ResultDetails, + size: number, + ) => Promise; + generateReport: ( + resultsIds: string[], + metadata?: ReportMetadata, + ) => Promise<{ reportId: UUID; reportPath: string }>; + readConfigFile: () => Promise<{ + result?: SiteWhiteLabelConfig; + error: Error | null; + }>; + saveConfigFile: ( + config: Partial, + ) => Promise<{ result: SiteWhiteLabelConfig; error: Error | null }>; } export interface ReportPath { - reportID: string; - project: string; + reportID: string; + project: string; } export interface ReadResultsInput { - pagination?: Pagination; - project?: string; - testRun?: string; - tags?: string[]; - search?: string; + pagination?: Pagination; + project?: string; + testRun?: string; + tags?: string[]; + search?: string; } export interface ReadResultsOutput { - results: Result[]; - total: number; + results: Result[]; + total: number; } export interface ReadReportsInput { - pagination?: Pagination; - project?: string; - ids?: string[]; - search?: string; + pagination?: Pagination; + project?: string; + ids?: string[]; + search?: string; } export interface ReadReportsOutput { - reports: ReportHistory[]; - total: number; + reports: ReportHistory[]; + total: number; } export interface ReadReportsHistory { - reports: ReportHistory[]; - total: number; + reports: ReportHistory[]; + total: number; } // For custom user fields export interface ResultDetails { - [key: string]: string; + [key: string]: string; } export type Result = { - resultID: UUID; - title?: string; - createdAt: string; - project: string; - size: string; - sizeBytes: number; + resultID: UUID; + title?: string; + createdAt: string; + project: string; + size: string; + sizeBytes: number; } & ResultDetails; export type Report = { - reportID: string; - title?: string; - project: string; - reportUrl: string; - createdAt: Date; - size: string; - sizeBytes: number; + reportID: string; + title?: string; + project: string; + reportUrl: string; + createdAt: Date; + size: string; + sizeBytes: number; }; export type ReportHistory = Report & ReportInfo; -export const isReportHistory = (report: Report | ReportHistory | undefined): report is ReportHistory => - !!report && typeof report === 'object' && 'stats' in report; +export const isReportHistory = ( + report: Report | ReportHistory | undefined, +): report is ReportHistory => + !!report && typeof report === "object" && "stats" in report; export type TestHistory = Report & ReportTest; -export type ReportMetadata = Partial<{ title: string; project: string; playwrightVersion?: string }> & - Record; +export type ReportMetadata = Partial<{ + title: string; + project: string; + playwrightVersion?: string; +}> & + Record; export interface ServerDataInfo { - dataFolderSizeinMB: string; - numOfResults: number; - resultsFolderSizeinMB: string; - numOfReports: number; - reportsFolderSizeinMB: string; + dataFolderSizeinMB: string; + numOfResults: number; + resultsFolderSizeinMB: string; + numOfReports: number; + reportsFolderSizeinMB: string; } diff --git a/app/settings/components/DatabaseInfo.tsx b/app/settings/components/DatabaseInfo.tsx deleted file mode 100644 index 7f3ff217..00000000 --- a/app/settings/components/DatabaseInfo.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import { Button } from '@heroui/react'; -import { toast } from 'sonner'; -import { useQueryClient } from '@tanstack/react-query'; - -import useMutation from '@/app/hooks/useMutation'; -import { invalidateCache } from '@/app/lib/query-cache'; -import { DatabaseStats } from '@/app/types'; - -interface DatabaseInfoProps { - stats?: DatabaseStats; -} - -export default function DatabaseInfo({ stats }: Readonly) { - const queryClient = useQueryClient(); - const { - mutate: cacheRefresh, - isPending, - error, - } = useMutation('/api/cache/refresh', { - method: 'POST', - onSuccess: () => { - invalidateCache(queryClient, { queryKeys: ['/api'] }); - toast.success(`db refreshed successfully`); - }, - }); - - return ( -
-

Size: {stats?.sizeOnDisk ?? 'n/a'}

-

RAM: {stats?.estimatedRAM}

-

Results: {stats?.results}

-

Reports: {stats?.reports}

- - {error && toast.error(error.message)} -
- ); -} diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..d7968e00 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,24 @@ +# Auth configuration +AUTH_SECRET= +AUTH_URL=http://localhost:3000 + +# API token details +API_TOKEN='my-api-token' +UI_AUTH_EXPIRE_HOURS='2' + +# Storage details +DATA_STORAGE=fs # could be s3 + +# S3 related configuration if DATA_STORAGE is "s3" +S3_ENDPOINT="s3.endpoint" +S3_ACCESS_KEY="some_access_key" +S3_SECRET_KEY="some_secret_key" +S3_PORT=9000 # optional +S3_REGION="us-east-1" +S3_BUCKET="bucket_name" # by default "playwright-reports-server" +S3_BATCH_SIZE=10 # by default 10 +S3_MULTIPART_CHUNK_SIZE_MB=25 # by default 25MB, controls chunk size for multipart uploads to reduce RAM usage + +# Server configuration +PORT=3001 +HOST=0.0.0.0 diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..5b7abbeb --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6439 @@ +{ + "name": "playwright-reports-server-backend", + "version": "5.7.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright-reports-server-backend", + "version": "5.7.4", + "dependencies": { + "@aws-sdk/client-s3": "^3.932.0", + "@aws-sdk/s3-request-presigner": "^3.932.0", + "@fastify/cookie": "^10.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/jwt": "^9.0.1", + "@fastify/multipart": "^9.0.1", + "@fastify/static": "^8.0.2", + "@playwright/test": "latest", + "better-sqlite3": "^12.4.1", + "busboy": "^1.6.0", + "croner": "^9.0.0", + "envalid": "^8.0.0", + "fastify": "^5.2.0", + "get-folder-size": "5.0.0", + "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", + "mime": "^4.0.4", + "sharp": "^0.33.5", + "uuid": "^10.0.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/busboy": "^1.5.4", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": ">=22.10.2", + "@types/uuid": "^10.0.0", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", + "typescript": "5.2.2", + "vite": "^6.0.3" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.943.0.tgz", + "integrity": "sha512-UOX8/1mmNaRmEkxoIVP2+gxd5joPJqz+fygRqlIXON1cETLGoctinMwQs7qU8g8hghm76TU2G6ZV6sLH8cySMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/credential-provider-node": "3.943.0", + "@aws-sdk/middleware-bucket-endpoint": "3.936.0", + "@aws-sdk/middleware-expect-continue": "3.936.0", + "@aws-sdk/middleware-flexible-checksums": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-location-constraint": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-sdk-s3": "3.943.0", + "@aws-sdk/middleware-ssec": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/eventstream-serde-browser": "^4.2.5", + "@smithy/eventstream-serde-config-resolver": "^4.3.5", + "@smithy/eventstream-serde-node": "^4.2.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-blob-browser": "^4.2.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/hash-stream-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/md5-js": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.943.0.tgz", + "integrity": "sha512-kOTO2B8Ks2qX73CyKY8PAajtf5n39aMe2spoiOF5EkgSzGV7hZ/HONRDyADlyxwfsX39Q2F2SpPUaXzon32IGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.943.0.tgz", + "integrity": "sha512-8CBy2hI9ABF7RBVQuY1bgf/ue+WPmM/hl0adrXFlhnhkaQP0tFY5zhiy1Y+n7V+5f3/ORoHBmCCQmcHDDYJqJQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.943.0.tgz", + "integrity": "sha512-WnS5w9fK9CTuoZRVSIHLOMcI63oODg9qd1vXMYb7QGLGlfwUm4aG3hdu7i9XvYrpkQfE3dzwWLtXF4ZBuL1Tew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.943.0.tgz", + "integrity": "sha512-SA8bUcYDEACdhnhLpZNnWusBpdmj4Vl67Vxp3Zke7SvoWSYbuxa+tiDiC+c92Z4Yq6xNOuLPW912ZPb9/NsSkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.943.0.tgz", + "integrity": "sha512-BcLDb8l4oVW+NkuqXMlO7TnM6lBOWW318ylf4FRED/ply5eaGxkQYqdGvHSqGSN5Rb3vr5Ek0xpzSjeYD7C8Kw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/credential-provider-env": "3.943.0", + "@aws-sdk/credential-provider-http": "3.943.0", + "@aws-sdk/credential-provider-login": "3.943.0", + "@aws-sdk/credential-provider-process": "3.943.0", + "@aws-sdk/credential-provider-sso": "3.943.0", + "@aws-sdk/credential-provider-web-identity": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.943.0.tgz", + "integrity": "sha512-9iCOVkiRW+evxiJE94RqosCwRrzptAVPhRhGWv4osfYDhjNAvUMyrnZl3T1bjqCoKNcETRKEZIU3dqYHnUkcwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.943.0.tgz", + "integrity": "sha512-14eddaH/gjCWoLSAELVrFOQNyswUYwWphIt+PdsJ/FqVfP4ay2HsiZVEIYbQtmrKHaoLJhiZKwBQRjcqJDZG0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.943.0", + "@aws-sdk/credential-provider-http": "3.943.0", + "@aws-sdk/credential-provider-ini": "3.943.0", + "@aws-sdk/credential-provider-process": "3.943.0", + "@aws-sdk/credential-provider-sso": "3.943.0", + "@aws-sdk/credential-provider-web-identity": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.943.0.tgz", + "integrity": "sha512-GIY/vUkthL33AdjOJ8r9vOosKf/3X+X7LIiACzGxvZZrtoOiRq0LADppdiKIB48vTL63VvW+eRIOFAxE6UDekw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.943.0.tgz", + "integrity": "sha512-1c5G11syUrru3D9OO6Uk+ul5e2lX1adb+7zQNyluNaLPXP6Dina6Sy6DFGRLu7tM8+M7luYmbS3w63rpYpaL+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.943.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/token-providers": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.943.0.tgz", + "integrity": "sha512-VtyGKHxICSb4kKGuaqotxso8JVM8RjCS3UYdIMOxUt9TaFE/CZIfZKtjTr+IJ7M0P7t36wuSUb/jRLyNmGzUUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.936.0.tgz", + "integrity": "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.936.0.tgz", + "integrity": "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.943.0.tgz", + "integrity": "sha512-J2oYbAQXTFEezs5m2Vij6H3w71K1hZfCtb85AsR/2Ovp/FjABMnK+Es1g1edRx6KuMTc9HkL/iGU4e+ek+qCZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.936.0.tgz", + "integrity": "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.943.0.tgz", + "integrity": "sha512-kd2mALfthU+RS9NsPS+qvznFcPnVgVx9mgmStWCPn5Qc5BTnx4UAtm+HPA+XZs+zxOopp+zmAfE4qxDHRVONBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.936.0.tgz", + "integrity": "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.943.0.tgz", + "integrity": "sha512-956n4kVEwFNXndXfhSAN5wO+KRgqiWEEY+ECwLvxmmO8uQ0NWOa8l6l65nTtyuiWzMX81c9BvlyNR5EgUeeUvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.943.0.tgz", + "integrity": "sha512-anFtB0p2FPuyUnbOULwGmKYqYKSq1M73c9uZ08jR/NCq6Trjq9cuF5TFTeHwjJyPRb4wMf2Qk859oiVfFqnQiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.943.0.tgz", + "integrity": "sha512-r79MlUb7jeydV0caSz/emoyttzDdxgSkS/8ZU3lawkoTTnWCt+1nB4VA2xzOAPzP2dSdwNVpuAdY7uD1t2f0wA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-format-url": "3.936.0", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.943.0.tgz", + "integrity": "sha512-KKvmxNQ/FZbM6ml6nKd8ltDulsUojsXnMJNgf1VHTcJEbADC/6mVWOq0+e9D0WP1qixUBEuMjlS2HqD5KoqwEg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.943.0.tgz", + "integrity": "sha512-cRKyIzwfkS+XztXIFPoWORuaxlIswP+a83BJzelX4S1gUZ7FcXB4+lj9Jxjn8SbQhR4TPU3Owbpu+S7pd6IRbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.936.0.tgz", + "integrity": "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.943.0.tgz", + "integrity": "sha512-gn+ILprVRrgAgTIBk2TDsJLRClzIOdStQFeFTcN0qpL8Z4GBCqMFhw7O7X+MM55Stt5s4jAauQ/VvoqmCADnQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/cookie": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-10.0.1.tgz", + "integrity": "sha512-NV/wbCUv4ETJ5KM1KMu0fLx0nSCm9idIxwg66NZnNbfPQH3rdbx6k0qRs5uy0y+MhBgvDudYRA30KlK659chyw==", + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.1", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.1.0.tgz", + "integrity": "sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/jwt": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-9.1.0.tgz", + "integrity": "sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "@lukeed/ms": "^2.0.2", + "fast-jwt": "^5.0.0", + "fastify-plugin": "^5.0.0", + "steed": "^1.1.3" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.3.0.tgz", + "integrity": "sha512-NpeKipTOjjL1dA7SSlRMrOWWtrE8/0yKOmeudkdQoEaz4sVDJw5MVdZIahsWhvpc3YTN7f04f9ep/Y65RKoOWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^3.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "secure-json-parse": "^4.0.0" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/send/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.18.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.6.tgz", + "integrity": "sha512-8Q/ugWqfDUEU1Exw71+DoOzlONJ2Cn9QA8VeeDzLLjzO/qruh9UKFzbszy4jXcIYgGofxYiT0t1TT6+CT/GupQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", + "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", + "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", + "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", + "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", + "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.6.tgz", + "integrity": "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.5.tgz", + "integrity": "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", + "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.13.tgz", + "integrity": "sha512-X4za1qCdyx1hEVVXuAWlZuK6wzLDv1uw1OY9VtaYy1lULl661+frY7FeuHdYdl7qAARUxH2yvNExU2/SmRFfcg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.6", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.13.tgz", + "integrity": "sha512-RzIDF9OrSviXX7MQeKOm8r/372KTyY8Jmp6HNKOOYlrguHADuM3ED/f4aCyNhZZFLG55lv5beBin7nL0Nzy1Dw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.9.tgz", + "integrity": "sha512-SUnZJMMo5yCmgjopJbiNeo1vlr8KvdnEfIHV9rlD77QuOGdRotIVBcOrBuMr+sI9zrnhtDtLP054bZVbpZpiQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.6", + "@smithy/middleware-endpoint": "^4.3.13", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.12.tgz", + "integrity": "sha512-TKc6FnOxFULKxLgTNHYjcFqdOYzXVPFFVm5JhI30F3RdhT7nYOtOsjgaOwfDRmA/3U66O9KaBQ3UHoXwayRhAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.15.tgz", + "integrity": "sha512-94NqfQVo+vGc5gsQ9SROZqOvBkGNMQu6pjXbnn8aQvBUhc31kx49gxlkBEqgmaZQHUUfdRUin5gK/HlHKmbAwg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.9", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", + "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/croner": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/croner/-/croner-9.1.0.tgz", + "integrity": "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==", + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/envalid": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.1.tgz", + "integrity": "sha512-vOUfHxAFFvkBjbVQbBfgnCO9d3GcNfMMTtVfgqSU2rQGMFEVqWy9GBuoSfHnwGu7EqR0/GeukQcL3KjFBaga9w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.1.1.tgz", + "integrity": "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-jwt": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-5.0.6.tgz", + "integrity": "sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.40.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastify": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.2.tgz", + "integrity": "sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", + "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-folder-size": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-5.0.0.tgz", + "integrity": "sha512-+fgtvbL83tSDypEK+T411GDBQVQtxv+qtQgbV+HVa/TYubqDhNd5ghH/D6cOHY9iC5/88GtOZB7WI8PXy2A3bg==", + "license": "MIT", + "bin": { + "get-folder-size": "bin/get-folder-size.js" + }, + "engines": { + "node": ">=18.11.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 00000000..d2268382 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,48 @@ +{ + "name": "playwright-reports-server-backend", + "version": "5.7.4", + "description": "Playwright Reports Server Backend", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.932.0", + "@aws-sdk/s3-request-presigner": "^3.932.0", + "@fastify/cookie": "^10.0.1", + "@fastify/cors": "^10.0.1", + "@fastify/jwt": "^9.0.1", + "@fastify/multipart": "^9.0.1", + "@fastify/static": "^8.0.2", + "@playwright/test": "latest", + "better-sqlite3": "^12.4.1", + "busboy": "^1.6.0", + "croner": "^9.0.0", + "envalid": "^8.0.0", + "fastify": "^5.2.0", + "get-folder-size": "5.0.0", + "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", + "mime": "^4.0.4", + "sharp": "^0.33.5", + "uuid": "^10.0.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/busboy": "^1.5.4", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": ">=22.10.2", + "@types/uuid": "^10.0.0", + "concurrently": "^9.2.1", + "tsx": "^4.21.0", + "typescript": "5.2.2", + "vite": "^6.0.3" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/backend/public/favicon.ico b/backend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..19d50f33815d9809439c041460a7ca3fdedc7ce2 GIT binary patch literal 262206 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 literal 0 HcmV?d00001 diff --git a/backend/public/logo.svg b/backend/public/logo.svg new file mode 100644 index 00000000..dddc9705 --- /dev/null +++ b/backend/public/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts new file mode 100644 index 00000000..f7313f85 --- /dev/null +++ b/backend/src/config/env.ts @@ -0,0 +1,49 @@ +import { cleanEnv, num, str } from "envalid"; + +export const env = cleanEnv(process.env, { + API_TOKEN: str({ desc: "API token for authorization", default: undefined }), + UI_AUTH_EXPIRE_HOURS: str({ + desc: "How much hours are allowed to keep auth session valid", + default: "2", + }), + AUTH_SECRET: str({ desc: "Secret for JWT", default: undefined }), + DATA_STORAGE: str({ desc: "Where to store data", default: "fs" }), + S3_ENDPOINT: str({ desc: "S3 endpoint", default: undefined }), + S3_ACCESS_KEY: str({ desc: "S3 access key", default: undefined }), + S3_SECRET_KEY: str({ desc: "S3 secret key", default: undefined }), + S3_PORT: num({ desc: "S3 port", default: undefined }), + S3_REGION: str({ desc: "S3 region", default: undefined }), + S3_BUCKET: str({ desc: "S3 bucket", default: "playwright-reports-server" }), + S3_BATCH_SIZE: num({ desc: "S3 batch size", default: 10 }), + S3_MULTIPART_CHUNK_SIZE_MB: num({ + desc: "S3 multipart upload chunk size in MB", + default: 25, + }), + RESULT_EXPIRE_DAYS: num({ + desc: "How much days to keep results", + default: undefined, + }), + RESULT_EXPIRE_CRON_SCHEDULE: str({ + desc: "Cron schedule for results cleanup", + default: "33 3 * * *", + }), + REPORT_EXPIRE_DAYS: num({ + desc: "How much days to keep reports", + default: undefined, + }), + REPORT_EXPIRE_CRON_SCHEDULE: str({ + desc: "Cron schedule for reports cleanup", + default: "44 4 * * *", + }), + JIRA_BASE_URL: str({ + desc: "Jira base URL (e.g., https://your-domain.atlassian.net)", + default: undefined, + }), + JIRA_EMAIL: str({ desc: "Jira user email", default: undefined }), + JIRA_API_TOKEN: str({ desc: "Jira API token", default: undefined }), + JIRA_PROJECT_KEY: str({ + desc: "Default Jira project key (optional)", + default: undefined, + }), + API_BASE_PATH: str({ desc: "Base path for the API", default: "" }), +}); diff --git a/backend/src/config/site.ts b/backend/src/config/site.ts new file mode 100644 index 00000000..94995c78 --- /dev/null +++ b/backend/src/config/site.ts @@ -0,0 +1,63 @@ +interface NavItem { + label: string; + href: string; +} + +export type HeaderLinks = Record; + +interface SiteConfig { + name: string; + description: string; + navItems: NavItem[]; + navMenuItems: NavItem[]; + links: HeaderLinks; +} + +export const defaultLinks: HeaderLinks = { + github: "https://github.com/CyborgTests/playwright-reports-server", + telegram: "https://t.me/js_for_testing/", + discord: "https://discord.gg/nuacYsb2yN", + cyborgTest: "https://www.cyborgtest.com/", +}; + +export const siteConfig: SiteConfig = { + name: "Playwright Reports Server", + description: "A server for Playwright Reports", + navItems: [ + { + label: "Reports", + href: "/reports", + }, + { + label: "Results", + href: "/results", + }, + { + label: "Trends", + href: "/trends", + }, + { + label: "Settings", + href: "/settings", + }, + ], + navMenuItems: [ + { + label: "Reports", + href: "/reports", + }, + { + label: "Results", + href: "/results", + }, + { + label: "Trends", + href: "/trends", + }, + { + label: "Settings", + href: "/settings", + }, + ], + links: defaultLinks, +}; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 00000000..a1f4038b --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,106 @@ +import { join } from "node:path"; +import fastifyCookie from "@fastify/cookie"; +import fastifyCors from "@fastify/cors"; +import fastifyJwt from "@fastify/jwt"; +import fastifyMultipart from "@fastify/multipart"; +import fastifyStatic from "@fastify/static"; +import Fastify from "fastify"; +import { lifecycle } from "./lib/service/lifecycle.js"; +import { registerApiRoutes } from "./routes/index.js"; + +const PORT = parseInt(process.env.PORT || "3001", 10); +const HOST = process.env.HOST || "0.0.0.0"; +const AUTH_SECRET = + process.env.AUTH_SECRET || "development-secret-change-in-production"; + +async function start() { + const fastify = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + }, + }); + + await fastify.register(fastifyCors, { + origin: process.env.CORS_ORIGIN || true, + credentials: true, + }); + + await fastify.register(fastifyCookie); + + await fastify.register(fastifyJwt, { + secret: AUTH_SECRET, + cookie: { + cookieName: "token", + signed: false, + }, + }); + + await fastify.register(fastifyMultipart); + + fastify.get("/api/ping", async () => { + return { + status: "ok", + timestamp: new Date().toISOString(), + }; + }); + + fastify.get("/api/health", async () => { + return { + status: "healthy", + timestamp: new Date().toISOString(), + }; + }); + + await registerApiRoutes(fastify); + + const dataDir = process.env.DATA_DIR || join(process.cwd(), "data"); + await fastify.register(fastifyStatic, { + root: dataDir, + prefix: "/data/", + decorateReply: false, + }); + + if (process.env.NODE_ENV === "production") { + const frontendDistPath = + process.env.FRONTEND_DIST || + join(process.cwd(), "..", "frontend", "dist"); + + await fastify.register(fastifyStatic, { + root: frontendDistPath, + prefix: "/assets/", + decorateReply: false, + }); + + // spa fallback for non-api routes + fastify.setNotFoundHandler(async (request, reply) => { + if (!request.url.startsWith("/api") && !request.url.startsWith("/data")) { + return reply.sendFile("index.html", frontendDistPath); + } + return reply.code(404).send({ error: "Not Found" }); + }); + } + + console.log("[server] Initializing databases and services..."); + await lifecycle.initialize(); + console.log("[server] Initialization complete"); + + const closeGracefully = async (signal: string) => { + fastify.log.info(`Received signal to terminate: ${signal}`); + await lifecycle.cleanup(); + await fastify.close(); + process.exit(0); + }; + + process.on("SIGINT", () => closeGracefully("SIGINT")); + process.on("SIGTERM", () => closeGracefully("SIGTERM")); + + try { + await fastify.listen({ port: PORT, host: HOST }); + fastify.log.info(`Server listening on http://${HOST}:${PORT}`); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +} + +await start(); diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts new file mode 100644 index 00000000..1a339eef --- /dev/null +++ b/backend/src/lib/auth.ts @@ -0,0 +1,7 @@ +export const isAuthorized = ({ + actualAuthToken, + expectedAuthToken, +}: { + actualAuthToken: string | null; + expectedAuthToken: string; +}) => actualAuthToken === expectedAuthToken; diff --git a/backend/src/lib/config.ts b/backend/src/lib/config.ts new file mode 100644 index 00000000..49dafb83 --- /dev/null +++ b/backend/src/lib/config.ts @@ -0,0 +1,37 @@ +import { defaultLinks } from "../config/site.js"; +import type { SiteWhiteLabelConfig } from "../types/index.js"; + +export const defaultConfig: SiteWhiteLabelConfig = { + title: "Cyborg Tests", + headerLinks: defaultLinks, + logoPath: "/logo.svg", + faviconPath: "/favicon.ico", + reporterPaths: [], + cron: { + resultExpireDays: Number(process.env.RESULT_EXPIRE_DAYS) ?? 30, + resultExpireCronSchedule: + process.env.RESULT_EXPIRE_CRON_SCHEDULE ?? "0 2 * * *", + reportExpireDays: Number(process.env.REPORT_EXPIRE_DAYS) ?? 90, + reportExpireCronSchedule: + process.env.REPORT_EXPIRE_CRON_SCHEDULE ?? "0 3 * * *", + }, + jira: { + baseUrl: process.env.JIRA_BASE_URL ?? "", + email: process.env.JIRA_EMAIL ?? "", + apiToken: process.env.JIRA_API_TOKEN ?? "", + projectKey: process.env.JIRA_PROJECT_KEY ?? "", + }, +}; + +export const noConfigErr = "no config"; + +export const isConfigValid = (config: any): config is SiteWhiteLabelConfig => { + return ( + !!config && + typeof config === "object" && + "title" in config && + "headerLinks" in config && + "logoPath" in config && + "faviconPath" in config + ); +}; diff --git a/backend/src/lib/constants.ts b/backend/src/lib/constants.ts new file mode 100644 index 00000000..eab7a709 --- /dev/null +++ b/backend/src/lib/constants.ts @@ -0,0 +1,3 @@ +export const serveReportRoute = "/api/serve"; + +export const defaultProjectName = "all"; diff --git a/backend/src/lib/network.ts b/backend/src/lib/network.ts new file mode 100644 index 00000000..edfd3cf2 --- /dev/null +++ b/backend/src/lib/network.ts @@ -0,0 +1,19 @@ +import { defaultProjectName } from "./constants"; + +export const buildUnauthorizedResponse = (): Response => { + return new Response("Unauthorized", { status: 401 }); +}; + +export const withQueryParams = ( + url: string, + params: Record, +): string => { + if (params?.project === defaultProjectName) { + delete params.project; + } + + const searchParams = new URLSearchParams(params); + const stringified = searchParams.toString(); + + return `${url}?${stringified}`; +}; diff --git a/backend/src/lib/parser/index.ts b/backend/src/lib/parser/index.ts new file mode 100644 index 00000000..5e6334a4 --- /dev/null +++ b/backend/src/lib/parser/index.ts @@ -0,0 +1,64 @@ +import JSZip from "jszip"; +import { withError } from "../../lib/withError.js"; +import type { ReportInfo } from "./types"; + +export * from "./types"; + +/** + * + * @param html HTML string of the Playwright report + * @description Parses the HTML report to extract the base64 encoded report data, decodes it + * There are two possible formats (at the moment): + * @example + * @example + * @returns + */ + +export const parse = async (html: string): Promise => { + const base64Prefix = "data:application/zip;base64,"; + const pattern = new RegExp(`${base64Prefix}([^";\\s]+)(?=[";\\s]|$)`); + const matches = RegExp(pattern).exec(html); + const match = matches?.at(0) ?? ""; + const base64String = match + .replace(base64Prefix, "") + .replace("", "") + .trim(); + + if (!base64String) { + throw Error("[report parser] no data found in the html report"); + } + + const zipData = Buffer.from(base64String, "base64"); + + const { result: zip, error } = await withError(JSZip.loadAsync(zipData)); + + if (error) { + throw Error(`[report parser] failed to load zip file: ${error.message}`); + } + + if (!zip) { + throw Error("[report parser] parsed report data is empty"); + } + + const reportFile = zip.file("report.json"); + + if (!reportFile) { + throw new Error("[report parser] no report.json file found in the zip"); + } + + const reportJson = await reportFile.async("string"); + + return JSON.parse(reportJson); +}; + +export const parseHtmlReport = async (html: string) => { + try { + return await parse(html); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(e.message); + } + + return null; + } +}; diff --git a/backend/src/lib/parser/types.ts b/backend/src/lib/parser/types.ts new file mode 100644 index 00000000..d9770b9e --- /dev/null +++ b/backend/src/lib/parser/types.ts @@ -0,0 +1,72 @@ +export interface ReportInfo { + metadata: ReportMetadata; + startTime: number; + duration: number; + files: ReportFile[]; + projectNames: string[]; + stats: ReportStats; +} + +interface ReportMetadata { + actualWorkers: number; + playwrightVersion?: string; +} + +export interface ReportStats { + total: number; + expected: number; + unexpected: number; + flaky: number; + skipped: number; + ok: boolean; +} + +export enum ReportTestOutcome { + Expected = "expected", + Unexpected = "unexpected", + Flaky = "flaky", + Skipped = "skipped", +} + +export interface ReportFile { + fileId: string; + fileName: string; + tests: ReportTest[]; + stats: ReportStats; +} + +export interface ReportTest { + testId: string; + title: string; + projectName: string; + location: ReportTestLocation; + duration: number; + annotations: string[]; + tags: string[]; + outcome: ReportTestOutcome; + path: string[]; + ok: boolean; + results: ReportTestResult[]; + createdAt?: Date; +} + +interface ReportTestLocation { + file: string; + line: number; + column: number; +} + +interface ReportTestAttachment { + name: string; + contentType: string; + path: string; +} + +interface ReportTestResult { + attachments: ReportTestAttachment[]; +} + +export interface ReportTestFilters { + outcomes?: ReportTestOutcome[]; + name?: string; +} diff --git a/backend/src/lib/pw.ts b/backend/src/lib/pw.ts new file mode 100644 index 00000000..608277c3 --- /dev/null +++ b/backend/src/lib/pw.ts @@ -0,0 +1,131 @@ +import { exec } from "node:child_process"; +import type { UUID } from "node:crypto"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import util from "node:util"; +import { defaultConfig } from "./config"; +import { storage } from "./storage"; +import { REPORTS_FOLDER, TMP_FOLDER } from "./storage/constants"; +import { createDirectory } from "./storage/folders"; +import type { ReportMetadata } from "./storage/types"; + +const execAsync = util.promisify(exec); + +export const isValidPlaywrightVersion = (version?: string): boolean => { + // stupid-simple validation to check that version follows semantic version format major.minor.patch + const versionPattern = /^\d+\.\d+\.\d+$/; + + return versionPattern.test(version ?? ""); +}; + +export const generatePlaywrightReport = async ( + reportId: UUID, + metadata: ReportMetadata, +): Promise<{ reportPath: string }> => { + const { project, playwrightVersion } = metadata; + + console.log(`[pw] generating Playwright report ${reportId}`); + + const reportPath = path.join(REPORTS_FOLDER, project ?? "", reportId); + + await createDirectory(reportPath); + + console.log(`[pw] report path: ${reportPath}`); + + const tempFolder = path.join(TMP_FOLDER, reportId); + + console.log(`[pw] merging reports from ${tempFolder}`); + + const versionTag = isValidPlaywrightVersion(playwrightVersion) + ? `@${playwrightVersion}` + : ""; + + console.log(`[pw] using playwright version tag: "${versionTag}"`); + + const { result: config } = await storage.readConfigFile(); + const customReporters = + config?.reporterPaths || defaultConfig.reporterPaths || []; + + const reporterArgs = ["html"]; + + if (customReporters.length > 0) { + const resolvedReporters = customReporters + .map((reporterPath) => { + if (path.isAbsolute(reporterPath)) { + return reporterPath; + } + + return path.resolve(process.cwd(), reporterPath); + }) + .filter((reporterPath) => { + if (existsSync(reporterPath)) { + return true; + } + console.warn(`[pw] reporter file not found: ${reporterPath}`); + + return false; + }); + + if (resolvedReporters.length > 0) { + reporterArgs.push(...resolvedReporters); + } + } + + /** + * Merge configuration used for server side reports merging. Needed to handle errors like this: + Network response was not ok: Error: Command failed: npx playwright merge-reports --reporter html /app/.tmp/99f690b3-aace-4293-a988-d5945eb0d259 + Error: Blob reports being merged were recorded with different test directories, and + merging cannot proceed. This may happen if you are merging reports from + machines with different environments, like different operating systems or + if the tests ran with different playwright configs. + + You can force merge by specifying a merge config file with "-c" option. If + you'd like all test paths to be correct, make sure 'testDir' in the merge config + file points to the actual tests location. + + Found directories: + /builds/_JRRzYANI/1/e2e/tests/ + /builds/_JRRzYANI/2/e2e/tests/ + /builds/_JRRzYANI/3/e2e/tests/ + at mergeConfigureEvents + */ + const mergeConfig = `export default { testDir: 'rootTestsDir' };`; + + await fs.writeFile(path.join(tempFolder, "merge.config.ts"), mergeConfig); + try { + // installing playwright into cache if not installed + const installResult = await execAsync( + `npx playwright${versionTag} --version`, + ); + + console.log(`[pw] npx cache for playwright${versionTag}`, installResult); + + const command = `npx playwright${versionTag} merge-reports --reporter ${reporterArgs.join(" --reporter ")} --config ${tempFolder}/merge.config.ts ${tempFolder}`; + + console.log("[pw] used merge config", mergeConfig); + console.log(`[pw] executing merging command: ${command}`); + const result = await execAsync(command, { + env: { + ...process.env, + // Avoid opening the report on server + PW_TEST_HTML_REPORT_OPEN: "never", + PLAYWRIGHT_HTML_REPORT: reportPath, + }, + }); + + console.log("[pw] merge result", result); + + if (result?.stderr) { + // got STDERR output while generating report - throwing error since we don't know what went wrong. + throw new Error(result?.stderr); + } + } catch (error) { + await fs.rm(reportPath, { recursive: true, force: true }); + throw error; + } + + return { + reportPath, + }; +}; diff --git a/backend/src/lib/query-cache.ts b/backend/src/lib/query-cache.ts new file mode 100644 index 00000000..b70a1318 --- /dev/null +++ b/backend/src/lib/query-cache.ts @@ -0,0 +1,27 @@ +import type { QueryClient } from "@tanstack/react-query"; + +interface InvalidateCacheOptions { + queryKeys?: string[]; + predicate?: string; +} + +export const invalidateCache = ( + client: QueryClient, + options: InvalidateCacheOptions, +) => { + if (options?.queryKeys) { + client.invalidateQueries({ queryKey: options.queryKeys }); + } + + if (options?.predicate) { + client.invalidateQueries({ + predicate: (q) => + q.queryKey.some( + (key) => + typeof key === "string" && + !!options.predicate && + key.startsWith(options.predicate), + ), + }); + } +}; diff --git a/backend/src/lib/schemas/index.ts b/backend/src/lib/schemas/index.ts new file mode 100644 index 00000000..e1737a00 --- /dev/null +++ b/backend/src/lib/schemas/index.ts @@ -0,0 +1,162 @@ +import { z } from "zod"; + +export const UUIDSchema = z.uuid(); + +export const PaginationSchema = z.object({ + limit: z.coerce.number().min(1).max(100).default(10), + offset: z.coerce.number().min(0).default(0), +}); + +export const ReportMetadataSchema = z.looseObject({ + project: z.string().optional(), + title: z.string().optional(), + playwrightVersion: z.string().optional(), + testRun: z.string().optional(), + tags: z.array(z.string()).optional(), +}); + +export const ReportHistorySchema = z.looseObject({ + reportID: UUIDSchema, + project: z.string(), + title: z.string().optional(), + createdAt: z.string(), + reportUrl: z.string(), + size: z.string().optional(), + sizeBytes: z.number(), + stats: z + .object({ + total: z.number(), + expected: z.number(), + unexpected: z.number(), + flaky: z.number(), + skipped: z.number(), + ok: z.boolean(), + }) + .optional(), +}); + +export const ResultDetailsSchema = z.looseObject({ + resultID: UUIDSchema, + project: z.string().optional(), + title: z.string().optional(), + createdAt: z.string(), + size: z.string().optional(), + sizeBytes: z.number(), + playwrightVersion: z.string().optional(), + testRun: z.string().optional(), + shardCurrent: z.number().optional(), + shardTotal: z.number().optional(), + triggerReportGeneration: z.coerce.boolean().optional(), +}); + +export const GenerateReportRequestSchema = z.object({ + resultsIds: z.array(z.string()).min(1), + project: z.string().optional(), + playwrightVersion: z.string().optional(), + title: z.string().optional(), +}); + +export const GenerateReportResponseSchema = z.object({ + reportId: z.string(), + reportUrl: z.string(), + metadata: ReportMetadataSchema, +}); + +export const ListReportsQuerySchema = z.object({ + project: z.string().default(""), + search: z.string().default(""), + limit: z.coerce.number().min(1).max(100).optional(), + offset: z.coerce.number().min(0).optional(), +}); + +export const ListReportsResponseSchema = z.object({ + reports: z.array(ReportHistorySchema), + total: z.number(), +}); + +export const DeleteReportsRequestSchema = z.object({ + reportsIds: z.array(z.string()).min(1), +}); + +export const DeleteReportsResponseSchema = z.object({ + message: z.string(), + reportsIds: z.array(z.string()), +}); + +export const ListResultsQuerySchema = z.object({ + project: z.string().default(""), + search: z.string().default(""), + tags: z.string().optional(), // comma-separated + limit: z.coerce.number().min(1).max(100).optional(), + offset: z.coerce.number().min(0).optional(), +}); + +export const ListResultsResponseSchema = z.object({ + results: z.array(ResultDetailsSchema), + total: z.number(), +}); + +export const DeleteResultsRequestSchema = z.object({ + resultsIds: z.array(z.string()).min(1), +}); + +export const DeleteResultsResponseSchema = z.object({ + message: z.string(), + resultsIds: z.array(z.string()), +}); + +export const GetReportParamsSchema = z.object({ + id: z.string(), +}); + +export const GetReportResponseSchema = ReportHistorySchema; + +export const UploadResultResponseSchema = z.object({ + message: z.string(), + data: z.object({ + resultID: UUIDSchema, + generatedReport: GenerateReportResponseSchema.optional().nullable(), + testRun: z.string().optional(), + }), +}); + +export const ServerInfoSchema = z.object({ + dataFolderSizeinMB: z.string(), + numOfResults: z.number(), + resultsFolderSizeinMB: z.string(), + numOfReports: z.number(), + reportsFolderSizeinMB: z.string(), +}); + +export const ConfigSchema = z.looseObject({ + siteName: z.string().optional(), + logoUrl: z.string().optional(), + theme: z.string().optional(), +}); + +export const ErrorResponseSchema = z.object({ + error: z.string(), +}); + +export type GenerateReportRequest = z.infer; +export type GenerateReportResponse = z.infer< + typeof GenerateReportResponseSchema +>; +export type ListReportsQuery = z.infer; +export type ListReportsResponse = z.infer; +export type DeleteReportsRequest = z.infer; +export type DeleteReportsResponse = z.infer; +export type ListResultsQuery = z.infer; +export type ListResultsResponse = z.infer; +export type DeleteResultsRequest = z.infer; +export type DeleteResultsResponse = z.infer; +export type GetReportParams = z.infer; +export type GetReportResponse = z.infer; +export type UploadResultResponse = z.infer; +export type ServerInfo = z.infer; +export type Config = z.infer; +export type ErrorResponse = z.infer; +export type ReportMetadata = z.infer; +export type ReportHistory = z.infer; +export type ResultDetails = z.infer; +export type Pagination = z.infer; diff --git a/backend/src/lib/service/cache/config.ts b/backend/src/lib/service/cache/config.ts new file mode 100644 index 00000000..c17323f5 --- /dev/null +++ b/backend/src/lib/service/cache/config.ts @@ -0,0 +1,56 @@ +import type { SiteWhiteLabelConfig } from "../../../types/index.js"; +import { defaultConfig } from "../../config.js"; +import { storage } from "../../storage/index.js"; + +const initiatedConfigDb = Symbol.for("playwright.reports.db.config"); +const instance = globalThis as typeof globalThis & { + [initiatedConfigDb]?: ConfigCache; +}; + +export class ConfigCache { + public initialized = false; + public config: SiteWhiteLabelConfig | undefined; + + private constructor() {} + + public static getInstance() { + instance[initiatedConfigDb] ??= new ConfigCache(); + + return instance[initiatedConfigDb]; + } + + public async init(): Promise { + if (this.initialized) { + return; + } + + console.log("[config cache] initializing cache"); + const { result, error } = await storage.readConfigFile(); + + if (error) { + console.error("[config cache] failed to read config file:", error); + console.warn("[config cache] using default config"); + + return; + } + + const cache = ConfigCache.getInstance(); + + cache.config = result ?? defaultConfig; + console.log("[config cache] initialized with config:", cache.config); + + this.initialized = true; + } + + public onChanged(config: SiteWhiteLabelConfig) { + this.config = config; + } + + public refresh(): void { + console.log("[config cache] refreshing cache"); + this.initialized = false; + this.config = undefined; + } +} + +export const configCache = ConfigCache.getInstance(); diff --git a/backend/src/lib/service/cron.ts b/backend/src/lib/service/cron.ts new file mode 100644 index 00000000..b260d420 --- /dev/null +++ b/backend/src/lib/service/cron.ts @@ -0,0 +1,176 @@ +import { Cron } from "croner"; +import { env } from "../../config/env.js"; +import { withError } from "../../lib/withError"; +import { service } from "."; + +const runningCron = Symbol.for("playwright.reports.cron.service"); +const instance = globalThis as typeof globalThis & { + [runningCron]?: CronService; +}; + +export class CronService { + public initialized = false; + + private clearResultsJob: Cron | undefined; + private clearReportsJob: Cron | undefined; + + public static getInstance() { + instance[runningCron] ??= new CronService(); + + return instance[runningCron]; + } + + private constructor() { + this.clearResultsJob = this.clearResultsTask(); + this.clearReportsJob = this.clearReportsTask(); + } + + public async restart() { + console.log("[cron-job] restarting cron tasks..."); + + this.clearResultsJob?.stop(); + this.clearReportsJob?.stop(); + + this.clearResultsJob = this.clearResultsTask(); + this.clearReportsJob = this.clearReportsTask(); + + this.initialized = false; + await this.init(); + } + + private isExpired(date: Date, days: number) { + const millisecondsDays = days * 24 * 60 * 60 * 1000; + + return date.getTime() < Date.now() - millisecondsDays; + } + + public async init() { + if (this.initialized) { + return; + } + const cfg = await service.getConfig(); + const reportExpireDays = + cfg.cron?.reportExpireDays || env.REPORT_EXPIRE_DAYS; + const resultExpireDays = + cfg.cron?.resultExpireDays || env.RESULT_EXPIRE_DAYS; + const reportExpireCronSchedule = + cfg.cron?.reportExpireCronSchedule || env.REPORT_EXPIRE_CRON_SCHEDULE; + const resultExpireCronSchedule = + cfg.cron?.resultExpireCronSchedule || env.RESULT_EXPIRE_CRON_SCHEDULE; + + console.log(`[cron-job] initiating cron tasks...`); + for (const schedule of [ + { + name: "reports", + cron: this.clearReportsJob, + expireDays: reportExpireDays, + expression: reportExpireCronSchedule, + }, + { + name: "results", + cron: this.clearResultsJob, + expireDays: resultExpireDays, + expression: resultExpireCronSchedule, + }, + ]) { + const message = schedule.cron + ? `found expiration task for ${schedule.name} older than ${schedule.expireDays} day(s) at "${schedule.expression}", starting...` + : `no expiration task for ${schedule.name}, skipping...`; + + console.log(`[cron-job] ${message}`); + + if (!schedule.cron) { + continue; + } + + if (schedule.cron.isRunning()) { + continue; + } + + schedule.cron?.resume(); + } + + this.initialized = true; + } + + private createJob(scheduleExpression: string, task: () => Promise) { + return new Cron( + scheduleExpression, + { catch: true, unref: true, paused: true, protect: true }, + task, + ); + } + + private clearReportsTask() { + const expireDays = env.REPORT_EXPIRE_DAYS; + + if (!expireDays) { + return; + } + + const scheduleExpression = env.REPORT_EXPIRE_CRON_SCHEDULE; + + return this.createJob(scheduleExpression, async () => { + const cfg = await service.getConfig(); + const expireDays = cfg.cron?.reportExpireDays || env.REPORT_EXPIRE_DAYS; + + console.log("[cron-job] starting outdated reports lookup..."); + const reportsOutput = await service.getReports(); + + const outdated = reportsOutput.reports.filter((report) => { + const createdDate = + typeof report.createdAt === "string" + ? new Date(report.createdAt) + : report.createdAt; + + return expireDays ? this.isExpired(createdDate, expireDays) : false; + }); + + console.log(`[cron-job] found ${outdated.length} outdated reports`); + + const outdatedIds = outdated.map((report) => report.reportID); + + const { error } = await withError(service.deleteReports(outdatedIds)); + + if (error) + console.error(`[cron-job] error deleting outdated results: ${error}`); + }); + } + + private clearResultsTask() { + const expireDays = env.RESULT_EXPIRE_DAYS; + + if (!expireDays) { + return; + } + const scheduleExpression = env.RESULT_EXPIRE_CRON_SCHEDULE; + + return this.createJob(scheduleExpression, async () => { + const cfg = await service.getConfig(); + const expireDays = cfg.cron?.resultExpireDays || env.RESULT_EXPIRE_DAYS; + + console.log("[cron-job] starting outdated results lookup..."); + const resultsOutput = await service.getResults(); + + const outdated = resultsOutput.results + .map((result) => ({ + ...result, + createdDate: new Date(result.createdAt), + })) + .filter((result) => + expireDays ? this.isExpired(result.createdDate, expireDays) : false, + ); + + console.log(`[cron-job] found ${outdated.length} outdated results`); + + const outdatedIds = outdated.map((result) => result.resultID); + + const { error } = await withError(service.deleteResults(outdatedIds)); + + if (error) + console.error(`[cron-job] error deleting outdated results: ${error}`); + }); + } +} + +export const cronService = CronService.getInstance(); diff --git a/backend/src/lib/service/db/db.ts b/backend/src/lib/service/db/db.ts new file mode 100644 index 00000000..dd820bcc --- /dev/null +++ b/backend/src/lib/service/db/db.ts @@ -0,0 +1,176 @@ +import fs from "node:fs"; +import path from "node:path"; + +import Database from "better-sqlite3"; + +const initiatedDb = Symbol.for("playwright.reports.db"); +const instance = globalThis as typeof globalThis & { + [initiatedDb]?: Database.Database; +}; + +export function createDatabase(): Database.Database { + if (instance[initiatedDb]) { + return instance[initiatedDb]; + } + + const dbDir = path.join(process.cwd(), "data"); + + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + const dbPath = path.join(dbDir, "metadata.db"); + + console.log(`[db] creating database at ${dbPath}`); + + const db = new Database(dbPath, { + verbose: undefined, // for debugging: console.log + }); + + db.pragma("journal_mode = WAL"); // better concurrency + db.pragma("synchronous = NORMAL"); // faster writes, still safe with WAL + db.pragma("cache_size = -8000"); // 8MB page cache (balance of speed and memory) + db.pragma("mmap_size = 134217728"); // 128MB memory-mapped I/O + db.pragma("temp_store = MEMORY"); // store temporary tables in RAM + db.pragma("foreign_keys = ON"); // enforce referential integrity + db.pragma("auto_vacuum = INCREMENTAL"); // manage file size + + console.log("[db] database is configured"); + + initializeSchema(db); + instance[initiatedDb] = db; + + return db; +} + +function initializeSchema(db: Database.Database): void { + console.log("[db] initializing schema"); + + db.exec(` + CREATE TABLE IF NOT EXISTS results ( + resultID TEXT PRIMARY KEY, + project TEXT NOT NULL, + title TEXT, + createdAt TEXT NOT NULL, + size TEXT, + sizeBytes INTEGER, + metadata TEXT, -- JSON string for additional metadata + updatedAt TEXT DEFAULT CURRENT_TIMESTAMP + ); + + -- Indexes for common queries + CREATE INDEX IF NOT EXISTS idx_results_ids ON results(resultID); + CREATE INDEX IF NOT EXISTS idx_results_project ON results(project); + CREATE INDEX IF NOT EXISTS idx_results_createdAt ON results(createdAt DESC); + CREATE INDEX IF NOT EXISTS idx_results_updatedAt ON results(updatedAt DESC); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS reports ( + reportID TEXT PRIMARY KEY, + project TEXT NOT NULL, + title TEXT, + createdAt TEXT NOT NULL, + reportUrl TEXT NOT NULL, + size TEXT, + sizeBytes INTEGER, + stats TEXT, -- JSON string for report stats + metadata TEXT, -- JSON string for additional metadata + updatedAt TEXT DEFAULT CURRENT_TIMESTAMP + ); + + -- Indexes for common queries + CREATE INDEX IF NOT EXISTS idx_reports_ids ON reports(reportID); + CREATE INDEX IF NOT EXISTS idx_reports_project ON reports(project); + CREATE INDEX IF NOT EXISTS idx_reports_createdAt ON reports(createdAt DESC); + CREATE INDEX IF NOT EXISTS idx_reports_updatedAt ON reports(updatedAt DESC); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS cache_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updatedAt TEXT DEFAULT CURRENT_TIMESTAMP + ); + `); + + console.log("[db] schema initialized"); +} + +export function getDatabase(): Database.Database { + if (!instance[initiatedDb]) { + return createDatabase(); + } + + return instance[initiatedDb]; +} + +export function closeDatabase(): void { + if (instance[initiatedDb]) { + console.log("[db] closing database connection"); + const db = getDatabase(); + + db.close(); + instance[initiatedDb] = undefined; + } +} + +export function getDatabaseStats(): { + results: number; + reports: number; + sizeOnDisk: string; + estimatedRAM: string; +} { + const db = getDatabase(); + + const resultsCount = db + .prepare("SELECT COUNT(*) as count FROM results") + .get() as { count: number }; + const reportsCount = db + .prepare("SELECT COUNT(*) as count FROM reports") + .get() as { count: number }; + + const stats = { + pageCount: db.pragma("page_count", { simple: true }) as number, + pageSize: db.pragma("page_size", { simple: true }) as number, + cacheSize: db.pragma("cache_size", { simple: true }) as number, + }; + + const dbSizeBytes = stats.pageCount * stats.pageSize; + const cacheSizeBytes = + Math.abs(stats.cacheSize) * (stats.cacheSize < 0 ? 1024 : stats.pageSize); + + return { + results: resultsCount.count, + reports: reportsCount.count, + sizeOnDisk: `${(dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, + estimatedRAM: `~${(cacheSizeBytes / 1024 / 1024).toFixed(2)} MB`, + }; +} + +export function clearAll(): void { + const db = getDatabase(); + + console.log("[db] clearing all data"); + + db.exec(` + DELETE FROM results; + DELETE FROM reports; + DELETE FROM cache_metadata; + `); + + db.exec("VACUUM;"); + + console.log("[db] cleared"); +} + +export function optimizeDB(): void { + const db = getDatabase(); + + console.log("[db] optimizing database"); + + db.exec("ANALYZE;"); + db.exec("PRAGMA incremental_vacuum;"); + + console.log("[db] optimization complete"); +} diff --git a/backend/src/lib/service/db/forceInit.ts b/backend/src/lib/service/db/forceInit.ts new file mode 100644 index 00000000..46800003 --- /dev/null +++ b/backend/src/lib/service/db/forceInit.ts @@ -0,0 +1,23 @@ +import { reportDb, resultDb } from "@/lib/service/db"; +import { withError } from "@/lib/withError"; + +export const forceInitDatabase = async () => { + const { error: cleanupError } = await withError( + Promise.all([reportDb.clear(), resultDb.clear()]), + ); + + if (cleanupError) { + throw new Error(`failed to clear db: ${cleanupError.message}`); + } + + reportDb.initialized = false; + resultDb.initialized = false; + + const { error } = await withError( + Promise.all([reportDb.init(), resultDb.init()]), + ); + + if (error) { + throw new Error(`failed to initialize db: ${error.message}`); + } +}; diff --git a/backend/src/lib/service/db/index.ts b/backend/src/lib/service/db/index.ts new file mode 100644 index 00000000..41ef6fcd --- /dev/null +++ b/backend/src/lib/service/db/index.ts @@ -0,0 +1,4 @@ +export * from "./db"; +export * from "./forceInit"; +export * from "./reports.sqlite"; +export * from "./results.sqlite"; diff --git a/backend/src/lib/service/db/reports.sqlite.ts b/backend/src/lib/service/db/reports.sqlite.ts new file mode 100644 index 00000000..45e06a43 --- /dev/null +++ b/backend/src/lib/service/db/reports.sqlite.ts @@ -0,0 +1,385 @@ +import type Database from "better-sqlite3"; +import { storage } from "../../storage/index.js"; +import type { + ReadReportsInput, + ReadReportsOutput, + ReportHistory, +} from "../../storage/types.js"; +import { withError } from "../../withError"; +import { getDatabase } from "./db"; + +const initiatedReportsDb = Symbol.for("playwright.reports.db.reports"); +const instance = globalThis as typeof globalThis & { + [initiatedReportsDb]?: ReportDatabase; +}; + +export class ReportDatabase { + public initialized = false; + private readonly db = getDatabase(); + + private readonly insertStmt: Database.Statement< + [ + string, + string, + string | null, + string, + string, + string | null, + number, + string | null, + string, + ] + >; + private readonly updateStmt: Database.Statement< + [ + string, + string | null, + string, + string | null, + number, + string | null, + string, + string, + ] + >; + private readonly deleteStmt: Database.Statement<[string]>; + private readonly getByIDStmt: Database.Statement<[string]>; + private readonly getAllStmt: Database.Statement<[]>; + private readonly getByProjectStmt: Database.Statement<[string]>; + private readonly searchStmt: Database.Statement< + [string, string, string, string] + >; + + private constructor() { + this.insertStmt = this.db.prepare(` + INSERT OR REPLACE INTO reports (reportID, project, title, createdAt, reportUrl, size, sizeBytes, stats, metadata, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `); + + this.updateStmt = this.db.prepare(` + UPDATE reports + SET project = ?, title = ?, reportUrl = ?, size = ?, sizeBytes = ?, stats = ?, metadata = ?, updatedAt = CURRENT_TIMESTAMP + WHERE reportID = ? + `); + + this.deleteStmt = this.db.prepare("DELETE FROM reports WHERE reportID = ?"); + + this.getByIDStmt = this.db.prepare( + "SELECT * FROM reports WHERE reportID = ?", + ); + + this.getAllStmt = this.db.prepare( + "SELECT * FROM reports ORDER BY createdAt DESC", + ); + + this.getByProjectStmt = this.db.prepare( + "SELECT * FROM reports WHERE project = ? ORDER BY createdAt DESC", + ); + + this.searchStmt = this.db.prepare(` + SELECT * FROM reports + WHERE title LIKE ? OR reportID LIKE ? OR project LIKE ? OR metadata LIKE ? + ORDER BY createdAt DESC + `); + } + + public static getInstance(): ReportDatabase { + instance[initiatedReportsDb] ??= new ReportDatabase(); + + return instance[initiatedReportsDb]; + } + + public async init() { + if (this.initialized) { + return; + } + + console.log("[report db] initializing SQLite for reports"); + const { result, error } = await withError(storage.readReports()); + + if (error) { + console.error("[report db] failed to read reports:", error); + + return; + } + + if (!result?.reports?.length) { + console.log("[report db] no reports to store"); + this.initialized = true; + + return; + } + + console.log(`[report db] caching ${result.reports.length} reports`); + + const insertMany = this.db.transaction((reports: ReportHistory[]) => { + for (const report of reports) { + this.insertReport(report); + } + }); + + insertMany(result.reports as ReportHistory[]); + + this.initialized = true; + console.log("[report db] initialization complete"); + } + + private insertReport(report: ReportHistory): void { + const { + reportID, + project, + title, + createdAt, + reportUrl, + size, + sizeBytes, + stats, + ...metadata + } = report; + + const createdAtStr = + createdAt instanceof Date + ? createdAt.toDateString() + : typeof createdAt === "string" + ? createdAt + : String(createdAt); + + this.insertStmt.run( + reportID, + project || "", + title || null, + createdAtStr, + reportUrl, + size || null, + sizeBytes || 0, + stats ? JSON.stringify(stats) : null, + JSON.stringify(metadata), + ); + } + + public onDeleted(reportIds: string[]) { + console.log(`[report db] deleting ${reportIds.length} reports`); + + const deleteMany = this.db.transaction((ids: string[]) => { + for (const id of ids) { + this.deleteStmt.run(id); + } + }); + + deleteMany(reportIds); + } + + public onCreated(report: ReportHistory) { + console.log(`[report db] adding report ${report.reportID}`); + this.insertReport(report); + } + + public onUpdated(report: ReportHistory) { + console.log(`[report db] updating report ${report.reportID}`); + const { + reportID, + project, + title, + reportUrl, + size, + sizeBytes, + stats, + ...metadata + } = report; + + this.updateStmt.run( + project || "", + title || null, + reportUrl, + size || null, + sizeBytes || 0, + stats ? JSON.stringify(stats) : null, + JSON.stringify(metadata), + reportID, + ); + } + + public getAll(): ReportHistory[] { + const rows = this.getAllStmt.all() as Array<{ + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }>; + + return rows.map(this.rowToReport); + } + + public getByID(reportID: string): ReportHistory | undefined { + const row = this.getByIDStmt.get(reportID) as + | { + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + } + | undefined; + + return row ? this.rowToReport(row) : undefined; + } + + public getByProject(project: string): ReportHistory[] { + const rows = this.getByProjectStmt.all(project) as Array<{ + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }>; + + return rows.map(this.rowToReport); + } + + public search(query: string): ReportHistory[] { + const searchPattern = `%${query}%`; + const rows = this.searchStmt.all( + searchPattern, + searchPattern, + searchPattern, + searchPattern, + ) as Array<{ + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }>; + + return rows.map(this.rowToReport); + } + + public getCount(): number { + const result = this.db + .prepare("SELECT COUNT(*) as count FROM reports") + .get() as { count: number }; + + return result.count; + } + + public clear(): void { + console.log("[report db] clearing all reports"); + this.db.prepare("DELETE FROM reports").run(); + } + + public query(input?: ReadReportsInput): ReadReportsOutput { + let query = "SELECT * FROM reports"; + const params: string[] = []; + const conditions: string[] = []; + + if (input?.ids && input.ids.length > 0) { + conditions.push(`reportID IN (${input.ids.map(() => "?").join(", ")})`); + params.push(...input.ids); + } + + if (input?.project) { + conditions.push("project = ?"); + params.push(input.project); + } + + if (input?.search?.trim()) { + const searchTerm = `%${input.search.toLowerCase().trim()}%`; + + conditions.push( + "(LOWER(title) LIKE ? OR LOWER(reportID) LIKE ? OR LOWER(project) LIKE ? OR LOWER(metadata) LIKE ?)", + ); + params.push(searchTerm, searchTerm, searchTerm, searchTerm); + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += " ORDER BY createdAt DESC"; + + const countQuery = query.replace("SELECT *", "SELECT COUNT(*) as count"); + const countResult = this.db.prepare(countQuery).get(...params) as { + count: number; + }; + const total = countResult.count; + + if (input?.pagination) { + query += " LIMIT ? OFFSET ?"; + params.push( + input.pagination.limit.toString(), + input.pagination.offset.toString(), + ); + } + + const rows = this.db.prepare(query).all(...params) as Array<{ + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }>; + + return { + reports: rows.map((row) => this.rowToReport(row)), + total, + }; + } + + public async refresh(): Promise { + console.log("[report db] refreshing cache"); + this.clear(); + this.initialized = false; + // Re-initialize immediately by reading from filesystem + await this.init(); + } + + private rowToReport(row: { + reportID: string; + project: string; + title: string | null; + createdAt: string; + reportUrl: string; + size: string | null; + sizeBytes: number; + stats: string | null; + metadata: string; + }): ReportHistory { + const metadata = JSON.parse(row.metadata || "{}"); + const stats = row.stats ? JSON.parse(row.stats) : undefined; + + return { + reportID: row.reportID, + project: row.project, + title: row.title || undefined, + createdAt: row.createdAt, + reportUrl: row.reportUrl, + size: row.size || undefined, + sizeBytes: row.sizeBytes, + stats, + ...metadata, + }; + } +} + +export const reportDb = ReportDatabase.getInstance(); diff --git a/backend/src/lib/service/db/results.sqlite.ts b/backend/src/lib/service/db/results.sqlite.ts new file mode 100644 index 00000000..c934d5ae --- /dev/null +++ b/backend/src/lib/service/db/results.sqlite.ts @@ -0,0 +1,346 @@ +import type Database from "better-sqlite3"; +import { storage } from "../../storage/index.js"; +import type { + ReadResultsInput, + ReadResultsOutput, + Result, +} from "../../storage/types.js"; +import { withError } from "../../withError"; +import { getDatabase } from "./db"; + +const initiatedResultsDb = Symbol.for("playwright.reports.db.results"); +const instance = globalThis as typeof globalThis & { + [initiatedResultsDb]?: ResultDatabase; +}; + +export class ResultDatabase { + public initialized = false; + private readonly db = getDatabase(); + + private readonly insertStmt: Database.Statement< + [string, string, string | null, string, string | null, number, string] + >; + private readonly updateStmt: Database.Statement< + [string, string | null, string | null, number, string, string] + >; + private readonly deleteStmt: Database.Statement<[string]>; + private readonly getByIDStmt: Database.Statement<[string]>; + private readonly getAllStmt: Database.Statement<[]>; + private readonly getByProjectStmt: Database.Statement<[string]>; + private readonly searchStmt: Database.Statement< + [string, string, string, string] + >; + + private constructor() { + this.insertStmt = this.db.prepare(` + INSERT OR REPLACE INTO results (resultID, project, title, createdAt, size, sizeBytes, metadata, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `); + + this.updateStmt = this.db.prepare(` + UPDATE results + SET project = ?, title = ?, size = ?, sizeBytes = ?, metadata = ?, updatedAt = CURRENT_TIMESTAMP + WHERE resultID = ? + `); + + this.deleteStmt = this.db.prepare("DELETE FROM results WHERE resultID = ?"); + + this.getByIDStmt = this.db.prepare( + "SELECT * FROM results WHERE resultID = ?", + ); + + this.getAllStmt = this.db.prepare( + "SELECT * FROM results ORDER BY createdAt DESC", + ); + + this.getByProjectStmt = this.db.prepare( + "SELECT * FROM results WHERE project = ? ORDER BY createdAt DESC", + ); + + this.searchStmt = this.db.prepare(` + SELECT * FROM results + WHERE title LIKE ? OR resultID LIKE ? OR project LIKE ? OR metadata LIKE ? + ORDER BY createdAt DESC + `); + } + + public static getInstance(): ResultDatabase { + instance[initiatedResultsDb] ??= new ResultDatabase(); + + return instance[initiatedResultsDb]; + } + + public async init() { + if (this.initialized) { + return; + } + + console.log("[result db] initializing SQLite for results"); + const { result: resultsResponse, error } = await withError( + storage.readResults(), + ); + + if (error) { + console.error("[result db] failed to read results:", error); + + return; + } + + if (!resultsResponse?.results?.length) { + console.log("[result db] no results to store"); + this.initialized = true; + + return; + } + + console.log( + `[result db] caching ${resultsResponse.results.length} results`, + ); + + const insertMany = this.db.transaction((results: Result[]) => { + for (const result of results) { + this.insertResult(result); + } + }); + + insertMany(resultsResponse.results); + + this.initialized = true; + console.log("[result db] initialization complete"); + } + + private insertResult(result: Result): void { + const { + resultID, + project, + title, + createdAt, + size, + sizeBytes, + ...metadata + } = result; + + this.insertStmt.run( + resultID, + project || "", + title || null, + createdAt, + size || null, + sizeBytes || 0, + JSON.stringify(metadata), + ); + } + + public onDeleted(resultIds: string[]) { + console.log(`[result db] deleting ${resultIds.length} results`); + + const deleteMany = this.db.transaction((ids: string[]) => { + for (const id of ids) { + this.deleteStmt.run(id); + } + }); + + deleteMany(resultIds); + } + + public onCreated(result: Result) { + console.log(`[result db] adding result ${result.resultID}`); + this.insertResult(result); + } + + public onUpdated(result: Result) { + console.log(`[result db] updating result ${result.resultID}`); + const { resultID, project, title, size, sizeBytes, ...metadata } = result; + + this.updateStmt.run( + project || "", + title || null, + size || null, + sizeBytes || 0, + JSON.stringify(metadata), + resultID, + ); + } + + public getAll(): Result[] { + const rows = this.getAllStmt.all() as Array<{ + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }>; + + return rows.map(this.rowToResult); + } + + public getByID(resultID: string): Result | undefined { + const row = this.getByIDStmt.get(resultID) as + | { + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + } + | undefined; + + return row ? this.rowToResult(row) : undefined; + } + + public getByProject(project: string): Result[] { + const rows = this.getByProjectStmt.all(project) as Array<{ + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }>; + + return rows.map(this.rowToResult); + } + + public search(query: string): Result[] { + const searchPattern = `%${query}%`; + const rows = this.searchStmt.all( + searchPattern, + searchPattern, + searchPattern, + searchPattern, + ) as Array<{ + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }>; + + return rows.map(this.rowToResult); + } + + public getCount(): number { + const result = this.db + .prepare("SELECT COUNT(*) as count FROM results") + .get() as { count: number }; + + return result.count; + } + + public clear(): void { + console.log("[result db] clearing all results"); + this.db.prepare("DELETE FROM results").run(); + } + + public query(input?: ReadResultsInput): ReadResultsOutput { + let query = "SELECT * FROM results"; + const params: string[] = []; + const conditions: string[] = []; + + if (input?.project) { + conditions.push("project = ?"); + params.push(input.project); + } + + if (input?.testRun) { + conditions.push("metadata LIKE ?"); + params.push(`%"testRun":"${input.testRun}"%`); + } + + if (input?.tags && input.tags.length > 0) { + console.log("Filtering by tags:", input.tags); + + for (const tag of input.tags) { + const [key, value] = tag.split(":").map((part) => part.trim()); + + conditions.push("metadata LIKE ?"); + params.push(`%"${key}":"${value}"%`); + } + } + + if (input?.search?.trim()) { + const searchTerm = `%${input.search.toLowerCase().trim()}%`; + + conditions.push( + "(LOWER(title) LIKE ? OR LOWER(resultID) LIKE ? OR LOWER(project) LIKE ? OR LOWER(metadata) LIKE ?)", + ); + params.push(searchTerm, searchTerm, searchTerm, searchTerm); + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(" AND ")}`; + } + + query += " ORDER BY createdAt DESC"; + + const countQuery = query.replace("SELECT *", "SELECT COUNT(*) as count"); + const countResult = this.db.prepare(countQuery).get(...params) as { + count: number; + }; + const total = countResult.count; + + if (input?.pagination) { + query += " LIMIT ? OFFSET ?"; + params.push( + input.pagination.limit.toString(), + input.pagination.offset.toString(), + ); + } + + console.log("Executing query:", query, "with params:", params); + + const rows = this.db.prepare(query).all(...params) as Array<{ + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }>; + + return { + results: rows.map((row) => this.rowToResult(row)), + total, + }; + } + + public async refresh(): Promise { + console.log("[result db] refreshing cache"); + this.clear(); + this.initialized = false; + // Re-initialize immediately by reading from filesystem + await this.init(); + } + + private rowToResult(row: { + resultID: string; + project: string; + title: string | null; + createdAt: string; + size: string | null; + sizeBytes: number; + metadata: string; + }): Result { + const metadata = JSON.parse(row.metadata || "{}"); + + return { + resultID: row.resultID, + project: row.project, + title: row.title || undefined, + createdAt: row.createdAt, + size: row.size || undefined, + sizeBytes: row.sizeBytes, + ...metadata, + }; + } +} + +export const resultDb = ResultDatabase.getInstance(); diff --git a/backend/src/lib/service/index.ts b/backend/src/lib/service/index.ts new file mode 100644 index 00000000..db06a144 --- /dev/null +++ b/backend/src/lib/service/index.ts @@ -0,0 +1,422 @@ +import { type PassThrough, Readable } from "node:stream"; +import { env } from "../../config/env.js"; +import type { SiteWhiteLabelConfig } from "../../types/index.js"; +import { defaultConfig } from "../config.js"; +import { serveReportRoute } from "../constants"; +import { isValidPlaywrightVersion } from "../pw.js"; +import { DEFAULT_STREAM_CHUNK_SIZE } from "../storage/constants"; +import { bytesToString, getUniqueProjectsList } from "../storage/format"; +import { + type ReadReportsInput, + type ReadResultsInput, + type ReadResultsOutput, + type ReportMetadata, + type ReportPath, + type ResultDetails, + type ServerDataInfo, + storage, +} from "../storage/index.js"; +import type { S3 } from "../storage/s3.js"; +import { withError } from "../withError"; +import { configCache } from "./cache/config.js"; +import { reportDb, resultDb } from "./db/index.js"; +import { lifecycle } from "./lifecycle.js"; + +class Service { + private static instance: Service | null = null; + + public static getInstance(): Service { + Service.instance ??= new Service(); + return Service.instance; + } + + public async getReports(input?: ReadReportsInput) { + console.log(`[service] getReports`); + + return reportDb.query(input); + } + + public async getReport(id: string, path?: string) { + console.log(`[service] getReport ${id}`); + + const report = reportDb.getByID(id); + + if (!report && path) { + console.warn( + `[service] getReport ${id} - not found in db, fetching from storage`, + ); + const { result: reportFromStorage, error } = await withError( + storage.readReport(id, path), + ); + + if (error) { + console.error( + `[service] getReport ${id} - error fetching from storage: ${error.message}`, + ); + throw error; + } + + if (!reportFromStorage) { + throw new Error(`report ${id} not found`); + } + + return reportFromStorage; + } + + if (!report) { + throw new Error(`report ${id} not found`); + } + + return report; + } + + private async findLatestPlaywrightVersionFromResults(resultIds: string[]) { + for (const resultId of resultIds) { + const { result: results, error } = await withError( + this.getResults({ search: resultId }), + ); + + if (error || !results) { + continue; + } + + const [latestResult] = results.results; + + if (!latestResult) { + continue; + } + + const latestVersion = latestResult?.playwrightVersion; + + if (latestVersion) { + return latestVersion; + } + } + } + + private async findLatestPlaywrightVersion(resultIds: string[]) { + const versionFromResults = + await this.findLatestPlaywrightVersionFromResults(resultIds); + + if (versionFromResults) { + return versionFromResults; + } + + // just in case version not found in results, we can try to get it from latest reports + const { result: reportsArray, error } = await withError( + this.getReports({ pagination: { limit: 10, offset: 0 } }), + ); + + if (error || !reportsArray) { + return ""; + } + + const reportWithVersion = reportsArray.reports.find( + (report) => !!report.metadata?.playwrightVersion, + ); + + if (!reportWithVersion) { + return ""; + } + + return reportWithVersion.metadata.playwrightVersion; + } + + public async generateReport( + resultsIds: string[], + metadata?: ReportMetadata, + ): Promise<{ + reportId: string; + reportUrl: string; + metadata: ReportMetadata; + }> { + const version = isValidPlaywrightVersion(metadata?.playwrightVersion) + ? metadata?.playwrightVersion + : await this.findLatestPlaywrightVersion(resultsIds); + + const metadataWithVersion = { + ...(metadata ?? {}), + playwrightVersion: version ?? "", + }; + + const { reportId, reportPath } = await storage.generateReport( + resultsIds, + metadataWithVersion, + ); + + console.log( + `[service] reading report ${reportId} from path: ${reportPath}`, + ); + const { result: report, error } = await withError( + storage.readReport(reportId, reportPath), + ); + + if (error) { + throw new Error(`Failed to read generated report: ${error.message}`); + } + + if (!report) { + throw new Error(`Generated report ${reportId} not found`); + } + + reportDb.onCreated(report); + + const projectPath = metadata?.project + ? `${encodeURI(metadata.project)}/` + : ""; + const reportUrl = `${serveReportRoute}/${projectPath}${reportId}/index.html`; + + return { reportId, reportUrl, metadata: metadataWithVersion }; + } + + public async deleteReports(reportIDs: string[]) { + const entries: ReportPath[] = []; + + for (const id of reportIDs) { + const report = await this.getReport(id); + + entries.push({ reportID: id, project: report.project }); + } + + const { error } = await withError(storage.deleteReports(entries)); + + if (error) { + throw error; + } + + reportDb.onDeleted(reportIDs); + } + + public async getReportsProjects(): Promise { + const { reports } = await this.getReports(); + const projects = getUniqueProjectsList(reports); + + return projects; + } + + public async getResults( + input?: ReadResultsInput, + ): Promise { + console.log(`[results service] getResults`); + console.log(`querying results:`); + console.log(JSON.stringify(input, null, 2)); + + return resultDb.query(input); + } + + public async deleteResults(resultIDs: string[]): Promise { + console.log(`[service] deleteResults`); + console.log(`deleting results:`, resultIDs); + + const { error } = await withError(storage.deleteResults(resultIDs)); + + if (error) { + console.error( + `[service] deleteResults - storage deletion failed:`, + error, + ); + throw error; + } + + console.log( + `[service] deleteResults - storage deletion successful, removing from database cache`, + ); + resultDb.onDeleted(resultIDs); + console.log(`[service] deleteResults - database cache cleanup completed`); + } + + public async getPresignedUrl(fileName: string): Promise { + console.log(`[service] getPresignedUrl for ${fileName}`); + + if (env.DATA_STORAGE !== "s3") { + console.log(`[service] fs storage detected, no presigned URL needed`); + + return ""; + } + + console.log(`[service] s3 detected, generating presigned URL`); + + const { result: presignedUrl, error } = await withError( + (storage as S3).generatePresignedUploadUrl(fileName), + ); + + if (error) { + console.error(`[service] getPresignedUrl | error: ${error.message}`); + + return ""; + } + + if (!presignedUrl) { + console.error(`[service] getPresignedUrl | presigned URL is null or undefined`); + + return ""; + } + + return presignedUrl; + } + + public async saveResult( + filename: string, + stream: PassThrough, + presignedUrl?: string, + contentLength?: string, + ) { + if (!presignedUrl) { + console.log(`[service] saving result`); + + return await storage.saveResult(filename, stream); + } + + console.log( + `[service] using direct upload via presigned URL`, + presignedUrl, + ); + + const { error } = await withError( + fetch(presignedUrl, { + method: "PUT", + body: Readable.toWeb(stream, { + strategy: { + highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, + }, + }), + headers: { + "Content-Type": "application/zip", + "Content-Length": contentLength, + }, + duplex: "half", + } as RequestInit), + ); + + if (error) { + console.error(`[s3] saveResult | error: ${error.message}`); + throw error; + } + } + + public async saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, + ) { + const result = await storage.saveResultDetails( + resultID, + resultDetails, + size, + ); + + resultDb.onCreated(result); + + return result; + } + + public async getResultsProjects(): Promise { + const { results } = await this.getResults(); + const projects = getUniqueProjectsList(results); + + const reportProjects = await this.getReportsProjects(); + + return Array.from(new Set([...projects, ...reportProjects])); + } + + public async getResultsTags(project?: string): Promise { + const { results } = await this.getResults( + project ? { project } : undefined, + ); + + const notMetadataKeys = [ + "resultID", + "title", + "createdAt", + "size", + "sizeBytes", + "project", + ]; + const allTags = new Set(); + + results.forEach((result) => { + Object.entries(result).forEach(([key, value]) => { + if ( + !notMetadataKeys.includes(key) && + value !== undefined && + value !== null + ) { + allTags.add(`${key}: ${value}`); + } + }); + }); + + return Array.from(allTags).sort(); + } + + public async getServerInfo(): Promise { + console.log(`[service] getServerInfo`); + const canCalculateFromCache = + lifecycle.isInitialized() && reportDb.initialized && resultDb.initialized; + + if (!canCalculateFromCache) { + return await storage.getServerDataInfo(); + } + + const reports = reportDb.getAll(); + const results = resultDb.getAll(); + + const getTotalSizeBytes = (entity: T) => + entity.reduce((total, item) => total + item.sizeBytes, 0); + + const reportsFolderSize = getTotalSizeBytes(reports); + const resultsFolderSize = getTotalSizeBytes(results); + const dataFolderSize = reportsFolderSize + resultsFolderSize; + + return { + dataFolderSizeinMB: bytesToString(dataFolderSize), + numOfResults: results.length, + resultsFolderSizeinMB: bytesToString(resultsFolderSize), + numOfReports: reports.length, + reportsFolderSizeinMB: bytesToString(reportsFolderSize), + }; + } + + public async getConfig() { + if (lifecycle.isInitialized() && configCache.initialized) { + const cached = configCache.config; + + if (cached) { + console.log(`[service] using cached config`); + + return cached; + } + } + + const { result, error } = await storage.readConfigFile(); + + if (error) console.error(`[service] getConfig | error: ${error.message}`); + + return { ...defaultConfig, ...(result ?? {}) }; + } + + public async updateConfig(config: Partial) { + console.log(`[service] updateConfig`, config); + const { result, error } = await storage.saveConfigFile(config); + + if (error) { + throw error; + } + + configCache.onChanged(result); + + return result; + } + + public async refreshCache() { + console.log(`[service] refreshCache`); + + await reportDb.refresh(); + await resultDb.refresh(); + configCache.refresh(); + + return { message: "cache refreshed successfully" }; + } +} + +export const service = Service.getInstance(); diff --git a/backend/src/lib/service/jira.ts b/backend/src/lib/service/jira.ts new file mode 100644 index 00000000..f9a09525 --- /dev/null +++ b/backend/src/lib/service/jira.ts @@ -0,0 +1,310 @@ +import { env } from "../../config/env.js"; +import type { JiraConfig } from "../../types/index.js"; + +export interface JiraIssueFields { + summary: string; + description: + | string + | { + type: string; + version: number; + content: Array<{ + type: string; + content: Array<{ + type: string; + text: string; + }>; + }>; + }; + issuetype: { name?: string; id?: string }; + project: { key: string }; + [key: string]: unknown; +} + +export interface JiraCreateIssueRequest { + fields: JiraIssueFields; +} + +export interface JiraCreateIssueResponse { + id: string; + key: string; + self: string; +} + +export interface JiraErrorResponse { + errorMessages: string[]; + errors: Record; +} + +const initiatedJira = Symbol.for("playwright.reports.jira"); +const _instance = globalThis as typeof globalThis & { + [initiatedJira]?: JiraService; +}; + +export class JiraService { + private baseUrl: string; + private auth: string; + + private constructor(jiraConfig?: JiraConfig) { + const config = jiraConfig || { + baseUrl: env.JIRA_BASE_URL, + email: env.JIRA_EMAIL, + apiToken: env.JIRA_API_TOKEN, + projectKey: env.JIRA_PROJECT_KEY, + }; + + this.baseUrl = config.baseUrl || ""; + const email = config.email || ""; + const apiToken = config.apiToken || ""; + + if (!this.baseUrl || !email || !apiToken) { + throw new Error( + "Jira configuration is incomplete. Please configure Jira settings in the admin panel or set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.", + ); + } + + this.auth = Buffer.from(`${email}:${apiToken}`).toString("base64"); + } + + public static getInstance(jiraConfig?: JiraConfig): JiraService { + _instance[initiatedJira] ??= new JiraService(jiraConfig); + + return _instance[initiatedJira]; + } + + public static resetInstance(): void { + _instance[initiatedJira] = undefined; + } + + private async makeRequest( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + body?: any, + ): Promise { + const url = `${this.baseUrl}/rest/api/3${endpoint}`; + + const headers: Record = { + Authorization: `Basic ${this.auth}`, + Accept: "application/json", + "Content-Type": "application/json", + }; + + const requestOptions: RequestInit = { + method, + headers, + }; + + if (body) { + requestOptions.body = JSON.stringify(body); + } + + const response = await fetch(url, requestOptions); + + if (!response.ok) { + const errorData = (await response.json()) as JiraErrorResponse; + const errorMessage = + errorData.errorMessages?.join(", ") || + Object.values(errorData.errors || {}).join(", ") || + `HTTP ${response.status}: ${response.statusText}`; + + throw new Error(`Jira API error: ${errorMessage}`); + } + + return response.json() as T; + } + + public async createIssue( + summary: string, + description: string, + issueType: string, + projectKey: string, + testInfo?: { + testId: string; + testTitle: string; + testOutcome: string; + testLocation: { + file: string; + line: number; + column: number; + }; + }, + attachments?: Array<{ + name: string; + path: string; + contentType: string; + }>, + ): Promise { + const issueTypes = await this.getIssueTypes(projectKey); + + let availableIssueTypes = issueTypes; + + try { + const project = await this.getProject(projectKey); + + if (project.issueTypes && project.issueTypes.length > 0) { + availableIssueTypes = project.issueTypes; + } + } catch { + console.warn( + `Could not fetch project-specific issue types for ${projectKey}, using global issue types`, + ); + } + + const issueTypeObj = availableIssueTypes.find( + (it: any) => it.name === issueType, + ); + + if (!issueTypeObj) { + throw new Error( + `Issue type '${issueType}' not found. Available issue types: ${availableIssueTypes.map((it: any) => it.name).join(", ")}`, + ); + } + + const fields: JiraIssueFields = { + summary, + description: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: description, + }, + ], + }, + ], + }, + issuetype: { id: issueTypeObj.id }, + project: { key: projectKey }, + }; + + if (testInfo) { + const testInfoText = ` + Test Information: + - Test ID: ${testInfo.testId} + - Test Title: ${testInfo.testTitle} + - Test Outcome: ${testInfo.testOutcome} + - File Location: ${testInfo.testLocation.file}:${testInfo.testLocation.line}:${testInfo.testLocation.column} + `; + + if (typeof fields.description === "string") { + fields.description += testInfoText; + } else if (fields.description.content && fields.description.content[0]) { + fields.description.content[0].content.push({ + type: "text", + text: testInfoText, + }); + } + } + + const requestBody: JiraCreateIssueRequest = { + fields, + }; + + console.log("Jira request body:", JSON.stringify(requestBody, null, 2)); + + const issueResponse = await this.makeRequest( + "/issue", + "POST", + requestBody, + ); + + if (attachments && attachments.length > 0) { + for (const attachment of attachments) { + await this.addAttachment(issueResponse.key, attachment); + } + } + + return issueResponse; + } + + public async getProject(projectKey: string): Promise { + return this.makeRequest(`/project/${projectKey}`); + } + + public async getIssueTypes(projectKey: string): Promise { + return this.makeRequest(`/project/${projectKey}`); + } + + public async addAttachment( + issueKey: string, + attachment: { + name: string; + path: string; + contentType: string; + }, + ): Promise { + try { + const { storage } = await import("@/lib/storage"); + + let fileName = attachment.name; + + if (!fileName.includes(".")) { + const extension = attachment.contentType.split("/")[1]; + + if (extension) { + fileName = `${fileName}.${extension}`; + } + } + + const fileContent = await storage.readFile( + attachment.path, + attachment.contentType, + ); + + const fileBuffer = + typeof fileContent === "string" + ? Buffer.from(fileContent, "utf-8") + : fileContent; + + const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; + + const formData = Buffer.concat([ + Buffer.from(`--${boundary}\r\n`), + Buffer.from( + `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`, + ), + Buffer.from(`Content-Type: ${attachment.contentType}\r\n\r\n`), + fileBuffer, + Buffer.from(`\r\n--${boundary}--\r\n`), + ]); + + const attachmentUrl = `${this.baseUrl}/rest/api/3/issue/${issueKey}/attachments`; + + const response = await fetch(attachmentUrl, { + method: "POST", + headers: { + Authorization: `Basic ${this.auth}`, + "X-Atlassian-Token": "no-check", + "Content-Type": `multipart/form-data; boundary=${boundary}`, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + + throw new Error( + `Failed to attach file to JIRA issue: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const result = await response.json(); + + console.log( + `Successfully attached ${attachment.name} to issue ${issueKey}`, + ); + + return result; + } catch (error) { + console.error( + `Error attaching file ${attachment.name} to issue ${issueKey}:`, + error, + ); + throw error; + } + } +} diff --git a/backend/src/lib/service/lifecycle.ts b/backend/src/lib/service/lifecycle.ts new file mode 100644 index 00000000..a977c878 --- /dev/null +++ b/backend/src/lib/service/lifecycle.ts @@ -0,0 +1,70 @@ +import { configCache } from "./cache/config"; +import { cronService } from "./cron"; +import { reportDb, resultDb } from "./db"; + +const createdLifecycle = Symbol.for("playwright.reports.lifecycle"); +const instance = globalThis as typeof globalThis & { + [createdLifecycle]?: Lifecycle; +}; + +export class Lifecycle { + private initialized = false; + private initPromise?: Promise; + + public static getInstance(): Lifecycle { + instance[createdLifecycle] ??= new Lifecycle(); + + return instance[createdLifecycle]; + } + + public async initialize(): Promise { + if (this.initialized) return; + + this.initPromise ??= this._performInitialization(); + + return this.initPromise; + } + + private async _performInitialization(): Promise { + console.log("[lifecycle] Starting application initialization"); + + try { + await Promise.all([configCache.init(), reportDb.init(), resultDb.init()]); + console.log("[lifecycle] Databases initialized successfully"); + + if (!cronService.initialized) { + await cronService.init(); + console.log("[lifecycle] Cron service initialized successfully"); + } + + this.initialized = true; + console.log("[lifecycle] Application initialization complete"); + } catch (error) { + console.error("[lifecycle] Initialization failed:", error); + throw error; + } + } + + public isInitialized(): boolean { + return this.initialized; + } + + public async cleanup(): Promise { + if (!this.initialized) return; + + console.log("[lifecycle] Starting application cleanup"); + + try { + if (cronService.initialized) { + await cronService.restart(); + console.log("[lifecycle] Cron service stopped"); + } + + console.log("[lifecycle] Application cleanup complete"); + } catch (error) { + console.error("[lifecycle] Cleanup failed:", error); + } + } +} + +export const lifecycle = Lifecycle.getInstance(); diff --git a/backend/src/lib/storage/batch.ts b/backend/src/lib/storage/batch.ts new file mode 100644 index 00000000..cad1d5da --- /dev/null +++ b/backend/src/lib/storage/batch.ts @@ -0,0 +1,18 @@ +export async function processBatch( + ctx: unknown, + items: T[], + batchSize: number, + asyncAction: (item: T) => Promise, +): Promise { + const results: R[] = []; + + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + + const batchResults = await Promise.all(batch.map(asyncAction.bind(ctx))); + + results.push(...batchResults); + } + + return results; +} diff --git a/backend/src/lib/storage/constants.ts b/backend/src/lib/storage/constants.ts new file mode 100644 index 00000000..8cdc2953 --- /dev/null +++ b/backend/src/lib/storage/constants.ts @@ -0,0 +1,23 @@ +import path from "node:path"; + +export const DATA_PATH = "data"; +export const RESULTS_PATH = "results"; +export const REPORTS_PATH = "reports"; +export const CONFIG_FILENAME = "config.json"; + +export const APP_CONFIG_S3 = `${DATA_PATH}/${CONFIG_FILENAME}`; +export const RESULTS_BUCKET = `${DATA_PATH}/${RESULTS_PATH}`; +export const REPORTS_BUCKET = `${DATA_PATH}/${REPORTS_PATH}`; + +const CWD = process.cwd(); + +export const DATA_FOLDER = path.join(CWD, DATA_PATH); +export const APP_CONFIG = path.join(DATA_FOLDER, CONFIG_FILENAME); +export const PW_CONFIG = path.join(CWD, "playwright.config.ts"); +export const TMP_FOLDER = path.join(CWD, ".tmp"); +export const RESULTS_FOLDER = path.join(DATA_FOLDER, RESULTS_PATH); +export const REPORTS_FOLDER = path.join(DATA_FOLDER, REPORTS_PATH); + +export const REPORT_METADATA_FILE = "report-server-metadata.json"; + +export const DEFAULT_STREAM_CHUNK_SIZE = 512 * 1024; // 512KB diff --git a/backend/src/lib/storage/file.ts b/backend/src/lib/storage/file.ts new file mode 100644 index 00000000..9c0c24a6 --- /dev/null +++ b/backend/src/lib/storage/file.ts @@ -0,0 +1,17 @@ +import { REPORTS_BUCKET } from "./constants"; + +export const isUUID = (uuid?: string): boolean => { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + uuid ?? "", + ); +}; + +export const getFileReportID = (filePath: string): string => { + const parts = filePath.split(REPORTS_BUCKET).pop()?.split("/") ?? []; + + const noProjectName = isUUID(parts?.at(1)); + + const reportIdIndex = noProjectName ? 1 : 2; + + return parts?.at(reportIdIndex) ?? ""; +}; diff --git a/backend/src/lib/storage/folders.ts b/backend/src/lib/storage/folders.ts new file mode 100644 index 00000000..811d8b84 --- /dev/null +++ b/backend/src/lib/storage/folders.ts @@ -0,0 +1,10 @@ +import fs from "node:fs/promises"; + +export async function createDirectory(dir: string) { + try { + await fs.access(dir); + } catch { + await fs.mkdir(dir, { recursive: true }); + console.log("Created directory:", dir); + } +} diff --git a/backend/src/lib/storage/format.ts b/backend/src/lib/storage/format.ts new file mode 100644 index 00000000..ec6cbbf3 --- /dev/null +++ b/backend/src/lib/storage/format.ts @@ -0,0 +1,18 @@ +import type { Report, Result } from "./types"; + +export const bytesToString = (bytes: number): string => { + const units = ["B", "KB", "MB", "GB", "TB"]; + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex++; + } + + return `${value.toFixed(2)} ${units[unitIndex]}`; +}; + +export const getUniqueProjectsList = (items: (Result | Report)[]): string[] => { + return Array.from(new Set(items.map((r) => r.project).filter(Boolean))); +}; diff --git a/backend/src/lib/storage/fs.ts b/backend/src/lib/storage/fs.ts new file mode 100644 index 00000000..b21bf202 --- /dev/null +++ b/backend/src/lib/storage/fs.ts @@ -0,0 +1,493 @@ +import { randomUUID } from "node:crypto"; +import { createWriteStream, type Dirent, type Stats } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import type { PassThrough } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +import getFolderSize from "get-folder-size"; +import type { SiteWhiteLabelConfig } from "../../types/index.js"; +import { defaultConfig, isConfigValid, noConfigErr } from "../config.js"; +import { serveReportRoute } from "../constants.js"; +import { parse } from "../parser/index.js"; +import { generatePlaywrightReport } from "../pw.js"; +import { withError } from "../withError.js"; +import { processBatch } from "./batch"; +import { + APP_CONFIG, + DATA_FOLDER, + DEFAULT_STREAM_CHUNK_SIZE, + REPORT_METADATA_FILE, + REPORTS_FOLDER, + REPORTS_PATH, + RESULTS_FOLDER, + TMP_FOLDER, +} from "./constants"; +import { createDirectory } from "./folders"; +import { bytesToString } from "./format"; +import type { + ReadReportsOutput, + ReportHistory, + ReportMetadata, + ReportPath, + Result, + ResultDetails, + ServerDataInfo, + Storage, +} from "./types.js"; + +async function createDirectoriesIfMissing() { + await createDirectory(RESULTS_FOLDER); + await createDirectory(REPORTS_FOLDER); + await createDirectory(TMP_FOLDER); +} + +const getSizeInMb = async (dir: string) => { + const sizeBytes = await getFolderSize.loose(dir); + + return bytesToString(sizeBytes); +}; + +export async function getServerDataInfo(): Promise { + await createDirectoriesIfMissing(); + const dataFolderSizeinMB = await getSizeInMb(DATA_FOLDER); + const resultsCount = await getResultsCount(); + const resultsFolderSizeinMB = await getSizeInMb(RESULTS_FOLDER); + const { total: reportsCount } = await readReports(); + const reportsFolderSizeinMB = await getSizeInMb(REPORTS_FOLDER); + + return { + dataFolderSizeinMB, + numOfResults: resultsCount, + resultsFolderSizeinMB, + numOfReports: reportsCount, + reportsFolderSizeinMB, + }; +} + +export async function readFile(targetPath: string, contentType: string | null) { + return await fs.readFile(path.join(REPORTS_FOLDER, targetPath), { + encoding: contentType === "text/html" ? "utf-8" : null, + }); +} + +async function getResultsCount() { + const files = await fs.readdir(RESULTS_FOLDER); + const zipFilesCount = files.filter((file) => file.endsWith(".zip")); + + return zipFilesCount.length; +} + +export async function readResults() { + await createDirectoriesIfMissing(); + const files = await fs.readdir(RESULTS_FOLDER); + + const stats = await processBatch< + string, + Stats & { filePath: string; size: string; sizeBytes: number } + >( + {}, + files.filter((file) => file.endsWith(".json")), + 20, + async (file) => { + const filePath = path.join(RESULTS_FOLDER, file); + + const stat = await fs.stat(filePath); + + const sizeBytes = await getFolderSize.loose( + filePath.replace(".json", ".zip"), + ); + + const size = bytesToString(sizeBytes); + + return Object.assign(stat, { filePath, size, sizeBytes }); + }, + ); + + const results = await processBatch< + Stats & { + filePath: string; + size: string; + sizeBytes: number; + }, + Result + >({}, stats, 10, async (entry) => { + const content = await fs.readFile(entry.filePath, "utf-8"); + + return { + size: entry.size, + sizeBytes: entry.sizeBytes, + ...JSON.parse(content), + }; + }); + + return { + results, + total: results.length, + }; +} + +function isMissingFileError(error?: Error | null) { + return error?.message.includes("ENOENT"); +} + +async function readOrParseReportMetadata( + id: string, + projectName: string, +): Promise { + const { result: metadataContent, error: metadataError } = await withError( + readFile(path.join(projectName, id, REPORT_METADATA_FILE), "utf-8"), + ); + + if (metadataError) + console.error( + `failed to read metadata for ${id}: ${metadataError.message}`, + ); + + const metadata = + metadataContent && !metadataError + ? JSON.parse(metadataContent.toString()) + : {}; + + if (!isMissingFileError(metadataError)) { + return metadata; + } + + console.log(`metadata file not found for ${id}, creating new metadata`); + try { + const parsed = await parseReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + { + project: projectName, + reportID: id, + }, + ); + + console.log(`parsed metadata for ${id}`); + + await saveReportMetadata( + path.join(REPORTS_FOLDER, projectName, id), + parsed, + ); + + Object.assign(metadata, parsed); + } catch (e) { + console.error( + `failed to create metadata for ${id}: ${(e as Error).message}`, + ); + } + + return metadata; +} + +export async function readReport( + reportID: string, + reportPath: string, +): Promise { + await createDirectoriesIfMissing(); + + console.log( + `[fs] reading report ${reportID} metadata from path: ${reportPath}`, + ); + + // Convert reportPath to relative path from REPORTS_FOLDER + const relativePath = path.relative(REPORTS_FOLDER, reportPath); + console.log(`[fs] reading report ${reportID} relative path: ${relativePath}`); + + const { result: metadataContent, error: metadataError } = await withError( + readFile(path.join(relativePath, REPORT_METADATA_FILE), "utf-8"), + ); + + if (metadataError) { + console.error( + `[fs] failed to read metadata for ${reportID}: ${metadataError.message}`, + ); + + return null; + } + + const metadata = metadataContent + ? JSON.parse(metadataContent.toString()) + : {}; + + return { + reportID, + project: metadata.project || "", + createdAt: new Date(metadata.createdAt), + size: metadata.size || "", + sizeBytes: metadata.sizeBytes || 0, + reportUrl: metadata.reportUrl || "", + ...metadata, + } as ReportHistory; +} + +export async function readReports(): Promise { + await createDirectoriesIfMissing(); + const entries = await fs.readdir(REPORTS_FOLDER, { + withFileTypes: true, + recursive: true, + }); + + const reportEntries = entries.filter( + (entry) => + !entry.isDirectory() && + entry.name === "index.html" && + !("parentPath" in entry && entry.parentPath?.endsWith("trace")), + ); + + const stats = await processBatch< + Dirent, + Stats & { filePath: string; createdAt: Date } + >({}, reportEntries, 20, async (file) => { + const filePath = + "parentPath" in file + ? file.parentPath + : path.join(REPORTS_FOLDER, (file as Dirent).name || ""); + const stat = await fs.stat(filePath); + + return Object.assign(stat, { filePath, createdAt: stat.birthtime }); + }); + + const reports = await processBatch< + Stats & { filePath: string; createdAt: Date }, + ReportHistory + >({}, stats, 10, async (file) => { + const id = path.basename(file.filePath); + const reportPath = path.dirname(file.filePath); + const parentDir = path.basename(reportPath); + const sizeBytes = await getFolderSize.loose(path.join(reportPath, id)); + const size = bytesToString(sizeBytes); + + const projectName = parentDir === REPORTS_PATH ? "" : parentDir; + + const metadata = await readOrParseReportMetadata(id, projectName); + + return { + reportID: id, + project: projectName, + createdAt: file.birthtime, + size, + sizeBytes, + reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ""}/${id}/index.html`, + ...metadata, + } as ReportHistory; + }); + + return { reports: reports, total: reports.length }; +} + +export async function deleteResults(resultsIds: string[]) { + await Promise.allSettled(resultsIds.map((id) => deleteResult(id))); +} + +export async function deleteResult(resultId: string) { + const resultPath = path.join(RESULTS_FOLDER, resultId); + + await Promise.allSettled([ + fs.unlink(`${resultPath}.json`), + fs.unlink(`${resultPath}.zip`), + ]); +} + +export async function deleteReports(reports: ReportPath[]) { + const paths = reports.map((report) => + report.project ? `${report.project}/${report.reportID}` : report.reportID, + ); + + await processBatch(undefined, paths, 10, async (path) => { + await deleteReport(path); + }); +} + +export async function deleteReport(reportId: string) { + const reportPath = path.join(REPORTS_FOLDER, reportId); + + await fs.rm(reportPath, { recursive: true, force: true }); +} + +export async function saveResult(filename: string, stream: PassThrough) { + await createDirectoriesIfMissing(); + const resultPath = path.join(RESULTS_FOLDER, filename); + + const writeable = createWriteStream(resultPath, { + encoding: "binary", + highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, + }); + + const { error: writeStreamError } = await withError( + pipeline(stream, writeable), + ); + + if (writeStreamError) { + throw new Error(`failed stream pipeline: ${writeStreamError.message}`); + } +} + +export async function saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, +): Promise { + await createDirectoriesIfMissing(); + + const metaData = { + resultID, + createdAt: new Date().toISOString(), + project: resultDetails?.project ?? "", + ...resultDetails, + size: bytesToString(size), + sizeBytes: size, + }; + + const { error: writeJsonError } = await withError( + fs.writeFile( + path.join(RESULTS_FOLDER, `${resultID}.json`), + JSON.stringify(metaData, null, 2), + { + encoding: "utf-8", + }, + ), + ); + + if (writeJsonError) { + throw new Error( + `failed to save result ${resultID} json file: ${writeJsonError.message}`, + ); + } + + return metaData as Result; +} + +export async function generateReport( + resultsIds: string[], + metadata?: ReportMetadata, +) { + await createDirectoriesIfMissing(); + + const reportId = randomUUID(); + const tempFolder = path.join(TMP_FOLDER, reportId); + + await fs.mkdir(tempFolder, { recursive: true }); + + try { + for (const id of resultsIds) { + await fs.copyFile( + path.join(RESULTS_FOLDER, `${id}.zip`), + path.join(tempFolder, `${id}.zip`), + ); + } + const generated = await generatePlaywrightReport(reportId, metadata!); + const info = await parseReportMetadata( + reportId, + generated.reportPath, + metadata, + ); + + await saveReportMetadata(generated.reportPath, info); + + return { reportId, reportPath: generated.reportPath }; + } finally { + await fs.rm(tempFolder, { recursive: true, force: true }); + } +} + +async function parseReportMetadata( + reportID: string, + reportPath: string, + metadata?: ReportMetadata, +): Promise { + const html = await fs.readFile(path.join(reportPath, "index.html"), "utf-8"); + const info = await parse(html as string); + + const content = Object.assign( + info, + { + reportID, + createdAt: new Date().toISOString(), + }, + metadata ?? {}, + ); + + return content; +} + +async function saveReportMetadata(reportPath: string, info: ReportMetadata) { + return fs.writeFile( + path.join(reportPath, REPORT_METADATA_FILE), + JSON.stringify(info, null, 2), + { + encoding: "utf-8", + }, + ); +} + +async function readConfigFile() { + const { error: accessConfigError } = await withError(fs.access(APP_CONFIG)); + + if (accessConfigError) { + return { result: defaultConfig, error: new Error(noConfigErr) }; + } + + const { result, error } = await withError(fs.readFile(APP_CONFIG, "utf-8")); + + if (error || !result) { + return { error }; + } + + try { + const parsed = JSON.parse(result); + + const isValid = isConfigValid(parsed); + + return isValid + ? { result: parsed, error: null } + : { error: new Error("invalid config") }; + } catch (e) { + return { + error: new Error( + `failed to parse config: ${e instanceof Error ? e.message : e}`, + ), + }; + } +} + +async function saveConfigFile(config: Partial) { + const { result: existingConfig, error: configError } = await readConfigFile(); + + const isConfigFailed = + !!configError && configError?.message !== noConfigErr && !existingConfig; + + if (isConfigFailed) { + console.error(`failed to read existing config: ${configError.message}`); + } + + const previousConfig = existingConfig ?? defaultConfig; + const uploadConfig = { ...previousConfig, ...config }; + + const { error } = await withError( + fs.writeFile(APP_CONFIG, JSON.stringify(uploadConfig, null, 2), { + flag: "w+", + }), + ); + + return { + result: uploadConfig, + error, + }; +} + +export const FS: Storage = { + getServerDataInfo, + readFile, + readResults, + readReports, + readReport, + deleteResults, + deleteReports, + saveResult, + saveResultDetails, + generateReport, + readConfigFile, + saveConfigFile, +}; diff --git a/backend/src/lib/storage/index.ts b/backend/src/lib/storage/index.ts new file mode 100644 index 00000000..1525fbfc --- /dev/null +++ b/backend/src/lib/storage/index.ts @@ -0,0 +1,7 @@ +export * from "./types"; + +import { env } from "../../config/env"; +import { FS } from "./fs"; +import { S3 } from "./s3"; + +export const storage = env.DATA_STORAGE === "s3" ? S3.getInstance() : FS; diff --git a/backend/src/lib/storage/pagination.ts b/backend/src/lib/storage/pagination.ts new file mode 100644 index 00000000..292e1234 --- /dev/null +++ b/backend/src/lib/storage/pagination.ts @@ -0,0 +1,14 @@ +export interface Pagination { + limit: number; + offset: number; +} + +export const parseFromRequest = (searchParams: URLSearchParams): Pagination => { + const limitQuery = searchParams.get("limit") ?? ""; + const offsetQuery = searchParams.get("offset") ?? ""; + + const limit = limitQuery ? Number.parseInt(limitQuery, 10) : 20; + const offset = offsetQuery ? Number.parseInt(offsetQuery, 10) : 0; + + return { limit, offset }; +}; diff --git a/backend/src/lib/storage/s3.ts b/backend/src/lib/storage/s3.ts new file mode 100644 index 00000000..cb7e0120 --- /dev/null +++ b/backend/src/lib/storage/s3.ts @@ -0,0 +1,1359 @@ +import { randomUUID, type UUID } from "node:crypto"; +import { createReadStream, createWriteStream } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import type { PassThrough, Readable } from "node:stream"; + +import { + type _Object, + AbortMultipartUploadCommand, + CompleteMultipartUploadCommand, + CreateBucketCommand, + CreateMultipartUploadCommand, + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + HeadObjectCommand, + ListObjectsV2Command, + PutObjectCommand, + S3Client, + UploadPartCommand, +} from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { env } from "../../config/env"; +import { withError } from "../../lib/withError"; +import type { SiteWhiteLabelConfig } from "../../types/index.js"; +import { defaultConfig, isConfigValid } from "../config.js"; +import { serveReportRoute } from "../constants.js"; +import { parse } from "../parser/index.js"; +import { generatePlaywrightReport } from "../pw.js"; +import { processBatch } from "./batch"; +import { + APP_CONFIG_S3, + DATA_FOLDER, + DATA_PATH, + REPORT_METADATA_FILE, + REPORTS_BUCKET, + REPORTS_FOLDER, + REPORTS_PATH, + RESULTS_BUCKET, + TMP_FOLDER, +} from "./constants"; +import { getFileReportID } from "./file"; +import { bytesToString } from "./format"; +import { + isReportHistory, + type ReadReportsOutput, + type ReadResultsOutput, + type Report, + type ReportHistory, + type ReportMetadata, + type ReportPath, + type Result, + type ResultDetails, + type ServerDataInfo, + type Storage, +} from "./types"; + +const createClient = () => { + const endPoint = env.S3_ENDPOINT; + const accessKey = env.S3_ACCESS_KEY; + const secretKey = env.S3_SECRET_KEY; + const port = env.S3_PORT; + const region = env.S3_REGION; + + if (!endPoint) { + throw new Error("S3_ENDPOINT is required"); + } + + if (!accessKey) { + throw new Error("S3_ACCESS_KEY is required"); + } + + if (!secretKey) { + throw new Error("S3_SECRET_KEY is required"); + } + + console.log("[s3] creating client"); + + const protocol = "https://"; + const endpointUrl = port + ? `${protocol}${endPoint}:${port}` + : `${protocol}${endPoint}`; + + const client = new S3Client({ + region: region || "us-east-1", + endpoint: endpointUrl, + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretKey, + }, + forcePathStyle: true, // required for S3-compatible services like Minio + }); + + return client; +}; + +export class S3 implements Storage { + private static instance: S3; + private readonly client: S3Client; + private readonly bucket: string; + private readonly batchSize: number; + + private constructor() { + this.client = createClient(); + this.bucket = env.S3_BUCKET; + this.batchSize = env.S3_BATCH_SIZE; + } + + public static getInstance() { + if (!S3.instance) { + S3.instance = new S3(); + } + + return S3.instance; + } + + private async ensureBucketExist() { + const { error } = await withError( + this.client.send(new HeadBucketCommand({ Bucket: this.bucket })), + ); + + if (!error) { + return; + } + + if (error.name === "NotFound") { + console.log(`[s3] bucket ${this.bucket} does not exist, creating...`); + + const { error } = await withError( + this.client.send( + new CreateBucketCommand({ + Bucket: this.bucket, + }), + ), + ); + + if (error) { + console.error("[s3] failed to create bucket:", error); + } + } + + console.error("[s3] failed to check that bucket exist:", error); + } + + private async write( + dir: string, + files: { + name: string; + content: Readable | Buffer | string; + size?: number; + }[], + ) { + await this.ensureBucketExist(); + for (const file of files) { + const filePath = path.join(dir, file.name); + + console.log(`[s3] writing ${filePath}`); + + const content = + typeof file.content === "string" + ? Buffer.from(file.content) + : file.content; + + await this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: path.normalize(filePath), + Body: content, + }), + ); + } + } + + private async read(targetPath: string, contentType?: string | null) { + await this.ensureBucketExist(); + console.log(`[s3] read ${targetPath}`); + + const remotePath = targetPath.includes(REPORTS_BUCKET) + ? targetPath + : `${REPORTS_BUCKET}/${targetPath}`; + + console.log(`[s3] reading from remote path: ${remotePath}`); + + const { result: response, error } = await withError( + this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ), + ); + + if (error ?? !response?.Body) { + return { result: null, error }; + } + + const stream = response.Body as Readable; + + const readStream = new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on("data", (chunk: Uint8Array) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + const fullContent = Buffer.concat(chunks); + + resolve(fullContent); + }); + + stream.on("error", (error) => { + console.error(`[s3] failed to read stream: ${error.message}`); + reject(error); + }); + }); + + const { result, error: readError } = await withError(readStream); + + return { + result: contentType === "text/html" ? result?.toString("utf-8") : result, + error: error ?? readError ?? null, + }; + } + + async clear(...path: string[]) { + console.log(`[s3] clearing ${path}`); + // avoid using "removeObjects" as it is not supported by every S3-compatible provider + // for example, Google Cloud Storage. + await processBatch( + this, + path, + this.batchSize, + async (object) => { + await this.client.send( + new DeleteObjectCommand({ + Bucket: this.bucket, + Key: object, + }), + ); + }, + ); + } + + async getFolderSize( + folderPath: string, + ): Promise<{ size: number; resultCount: number; indexCount: number }> { + let resultCount = 0; + let indexCount = 0; + let totalSize = 0; + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: folderPath, + ContinuationToken: continuationToken, + }), + ); + + for (const obj of response.Contents ?? []) { + if (obj.Key?.endsWith(".zip")) { + resultCount += 1; + } + + if ( + obj.Key?.endsWith("index.html") && + !obj.Key.includes("/trace/index.html") + ) { + indexCount += 1; + } + + totalSize += obj?.Size ?? 0; + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + return { size: totalSize, resultCount, indexCount }; + } + + async getServerDataInfo(): Promise { + await this.ensureBucketExist(); + console.log("[s3] getting server data"); + + const [results, reports] = await Promise.all([ + this.getFolderSize(RESULTS_BUCKET), + this.getFolderSize(REPORTS_BUCKET), + ]); + + const dataSize = results.size + reports.size; + + return { + dataFolderSizeinMB: bytesToString(dataSize), + numOfResults: results.resultCount, + resultsFolderSizeinMB: bytesToString(results.size), + numOfReports: reports.indexCount, + reportsFolderSizeinMB: bytesToString(reports.size), + }; + } + + async readFile( + targetPath: string, + contentType: string | null, + ): Promise { + console.log(`[s3] reading ${targetPath} | ${contentType}`); + const { result, error } = await this.read(targetPath, contentType); + + if (error) { + console.error(`[s3] failed to read file ${targetPath}: ${error.message}`); + throw new Error(`[s3] failed to read file: ${error.message}`); + } + + return result!; + } + + async readResults(): Promise { + await this.ensureBucketExist(); + + console.log("[s3] reading results"); + + const jsonFiles: _Object[] = []; + const resultSizes = new Map(); + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: RESULTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + if (file.Key.endsWith(".zip")) { + const resultID = path.basename(file.Key, ".zip"); + + resultSizes.set(resultID, file.Size ?? 0); + } + + if (file.Key.endsWith(".json")) { + jsonFiles.push(file); + } + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + console.log(`[s3] found ${jsonFiles.length} json files`); + + if (!jsonFiles) { + return { + results: [], + total: 0, + }; + } + + const results = await processBatch<_Object, Result>( + this, + jsonFiles, + this.batchSize, + async (file) => { + console.log(`[s3.batch] reading result: ${JSON.stringify(file)}`); + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: file.Key!, + }), + ); + + const stream = response.Body as Readable; + let jsonString = ""; + + for await (const chunk of stream) { + jsonString += chunk.toString(); + } + + const parsed = JSON.parse(jsonString); + + return parsed; + }, + ); + + return { + results: results.map((result) => { + const sizeBytes = resultSizes.get(result.resultID) ?? 0; + + return { + ...result, + sizeBytes, + size: result.size ?? bytesToString(sizeBytes), + }; + }) as Result[], + total: results.length, + }; + } + + async readReport( + reportID: string, + reportPath: string, + ): Promise { + await this.ensureBucketExist(); + + console.log(`[s3] reading report ${reportID} metadata`); + + const relativePath = path.relative(reportPath, REPORTS_BUCKET); + + const objectKey = path.join( + REPORTS_BUCKET, + relativePath, + REPORT_METADATA_FILE, + ); + + console.log(`[s3] checking existence of result: ${objectKey}`); + const { error: headError } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ), + ); + + if (headError) { + throw new Error(`failed to check ${objectKey}: ${headError.message}`); + } + + console.log(`[s3] downloading metadata file: ${objectKey}`); + const localFilePath = path.join(TMP_FOLDER, reportID, REPORT_METADATA_FILE); + + const { error: downloadError } = await withError( + (async () => { + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localFilePath); + + return new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + })(), + ); + + if (downloadError) { + console.error( + `[s3] failed to download ${objectKey}: ${downloadError.message}`, + ); + + throw new Error( + `failed to download ${objectKey}: ${downloadError.message}`, + ); + } + + try { + const content = await fs.readFile(localFilePath, "utf-8"); + + const metadata = JSON.parse(content); + + return isReportHistory(metadata) ? metadata : null; + } catch (e) { + console.error( + `[s3] failed to read or parse metadata file: ${(e as Error).message}`, + ); + + return null; + } + } + + async readReports(): Promise { + await this.ensureBucketExist(); + + console.log(`[s3] reading reports from external storage`); + + const reports: Report[] = []; + const reportSizes = new Map(); + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: REPORTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + const reportID = getFileReportID(file.Key); + + const newSize = (reportSizes.get(reportID) ?? 0) + (file.Size ?? 0); + + reportSizes.set(reportID, newSize); + + if (!file.Key.endsWith("index.html") || file.Key.includes("trace")) { + continue; + } + + const dir = path.dirname(file.Key); + const id = path.basename(dir); + const parentDir = path.basename(path.dirname(dir)); + + const projectName = parentDir === REPORTS_PATH ? "" : parentDir; + + const report = { + reportID: id, + project: projectName, + createdAt: file.LastModified ?? new Date(), + reportUrl: `${serveReportRoute}/${projectName ? encodeURIComponent(projectName) : ""}/${id}/index.html`, + size: "", + sizeBytes: 0, + }; + + reports.push(report); + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + const withMetadata = await this.getReportsMetadata( + reports as ReportHistory[], + ); + + return { + reports: withMetadata.map((report) => { + const sizeBytes = reportSizes.get(report.reportID) ?? 0; + + return { + ...report, + sizeBytes, + size: bytesToString(sizeBytes), + }; + }), + total: withMetadata.length, + }; + } + + async getReportsMetadata(reports: ReportHistory[]): Promise { + return await processBatch( + this, + reports, + this.batchSize, + async (report) => { + console.log(`[s3.batch] reading report ${report.reportID} metadata`); + + const { result: metadata, error: metadataError } = await withError( + this.readOrParseReportMetadata(report.reportID, report.project), + ); + + if (metadataError) { + console.error( + `[s3] failed to read or create metadata for ${report.reportID}: ${metadataError.message}`, + ); + + return report; + } + + if (!metadata) { + return report; + } + + return Object.assign(metadata, report); + }, + ); + } + + async readOrParseReportMetadata( + id: string, + projectName: string, + ): Promise { + const { result: metadataContent, error: metadataError } = await withError( + this.readFile( + path.join(REPORTS_BUCKET, projectName, id, REPORT_METADATA_FILE), + "utf-8", + ), + ); + + if (metadataError) + console.error( + `[s3] failed to read metadata for ${id}: ${metadataError.message}`, + ); + + const metadata = + metadataContent && !metadataError + ? JSON.parse(metadataContent.toString()) + : {}; + + if (isReportHistory(metadata)) { + console.log(`metadata found for report ${id}`); + + return metadata; + } + + console.log(`metadata file not found for ${id}, creating new metadata`); + try { + const { result: htmlContent, error: htmlError } = await withError( + this.readFile( + path.join(REPORTS_BUCKET, projectName, id, "index.html"), + "utf-8", + ), + ); + + if (htmlError) + console.error( + `[s3] failed to read index.html for ${id}: ${htmlError.message}`, + ); + + const created = await this.parseReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + { + project: projectName, + reportID: id, + }, + htmlContent?.toString(), + ); + + console.log( + `metadata object created for ${id}: ${JSON.stringify(created)}`, + ); + + await this.saveReportMetadata( + id, + path.join(REPORTS_FOLDER, projectName, id), + created, + ); + + Object.assign(metadata, created); + } catch (e) { + console.error( + `failed to create metadata for ${id}: ${(e as Error).message}`, + ); + } + + return metadata; + } + + async deleteResults(resultIDs: string[]): Promise { + const objects = resultIDs.flatMap((id) => [ + `${RESULTS_BUCKET}/${id}.json`, + `${RESULTS_BUCKET}/${id}.zip`, + ]); + + await withError(this.clear(...objects)); + } + + private async getReportObjects(reportsIDs: string[]): Promise { + const files: string[] = []; + + let continuationToken: string | undefined; + + do { + const response = await this.client.send( + new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: REPORTS_BUCKET, + ContinuationToken: continuationToken, + }), + ); + + for (const file of response.Contents ?? []) { + if (!file?.Key) { + continue; + } + + const reportID = path.basename(path.dirname(file.Key)); + + if (reportsIDs.includes(reportID)) { + files.push(file.Key); + } + } + + continuationToken = response.IsTruncated + ? response.NextContinuationToken + : undefined; + } while (continuationToken); + + return files; + } + + async deleteReports(reports: ReportPath[]): Promise { + const ids = reports.map((r) => r.reportID); + const objects = await this.getReportObjects(ids); + + await withError(this.clear(...objects)); + } + + async generatePresignedUploadUrl(fileName: string) { + await this.ensureBucketExist(); + const objectKey = path.join(RESULTS_BUCKET, fileName); + const expiry = 30 * 60; // 30 minutes + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }); + + return await getSignedUrl(this.client, command, { expiresIn: expiry }); + } + + async saveResult(filename: string, stream: PassThrough) { + await this.ensureBucketExist(); + + const chunkSizeMB = env.S3_MULTIPART_CHUNK_SIZE_MB; + const chunkSize = chunkSizeMB * 1024 * 1024; // bytes + + console.log( + `[s3] starting multipart upload for ${filename} with chunk size ${chunkSizeMB}MB`, + ); + + const remotePath = path.join(RESULTS_BUCKET, filename); + + const { UploadId: uploadID } = await this.client.send( + new CreateMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ); + + if (!uploadID) { + throw new Error( + "[s3] failed to initiate multipart upload: no UploadId received", + ); + } + + const uploadedParts: { PartNumber: number; ETag: string }[] = []; + let partNumber = 1; + const chunks: Buffer[] = []; + let currentSize = 0; + + try { + for await (const chunk of stream) { + console.log( + `[s3] received chunk of size ${(chunk.length / (1024 * 1024)).toFixed(2)}MB for ${filename}`, + ); + + chunks.push(chunk); + currentSize += chunk.length; + + while (currentSize >= chunkSize) { + const partData = Buffer.allocUnsafe(chunkSize); + let copied = 0; + + while (copied < chunkSize && chunks.length > 0) { + const currentChunk = chunks[0]; + const needed = chunkSize - copied; + const available = currentChunk.length; + + if (available <= needed) { + currentChunk.copy(partData, copied); + copied += available; + chunks.shift(); + } else { + currentChunk.copy(partData, copied, 0, needed); + copied += needed; + chunks[0] = currentChunk.subarray(needed); + } + } + + currentSize -= chunkSize; + + console.log( + `[s3] uploading part ${partNumber} (${(partData.length / (1024 * 1024)).toFixed(2)}MB) for ${filename}`, + ); + console.log( + `[s3] buffer state: ${chunks.length} chunks, ${(currentSize / (1024 * 1024)).toFixed(2)}MB remaining`, + ); + + stream.pause(); + + const uploadPartResult = await this.client.send( + new UploadPartCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + PartNumber: partNumber, + Body: partData, + }), + ); + + // explicitly clear the part data to help GC + partData.fill(0); + + console.log(`[s3] uploaded part ${partNumber}, resume reading`); + stream.resume(); + + if (!uploadPartResult.ETag) { + throw new Error( + `[s3] failed to upload part ${partNumber}: no ETag received`, + ); + } + + uploadedParts.push({ + PartNumber: partNumber, + ETag: uploadPartResult.ETag, + }); + + partNumber++; + } + } + + if (currentSize > 0) { + console.log( + `[s3] uploading final part ${partNumber} [${bytesToString(currentSize)}] for ${filename}`, + ); + + const finalPart = Buffer.allocUnsafe(currentSize); + let offset = 0; + + for (const chunk of chunks) { + chunk.copy(finalPart, offset); + offset += chunk.length; + } + + const uploadPartResult = await this.client.send( + new UploadPartCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + PartNumber: partNumber, + Body: finalPart, + }), + ); + + // explicitly clear buffer references + chunks.length = 0; + finalPart.fill(0); + + if (!uploadPartResult.ETag) { + throw new Error( + `[s3] failed to upload final part ${partNumber}: no ETag received`, + ); + } + + uploadedParts.push({ + PartNumber: partNumber, + ETag: uploadPartResult.ETag, + }); + } + + console.log( + `[s3] completing multipart upload for ${filename} with ${uploadedParts.length} parts`, + ); + + await this.client.send( + new CompleteMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + MultipartUpload: { + Parts: uploadedParts, + }, + }), + ); + + console.log( + `[s3] multipart upload completed successfully for ${filename}`, + ); + } catch (error) { + console.error( + `[s3] multipart upload failed, aborting: ${(error as Error).message}`, + ); + + await this.client.send( + new AbortMultipartUploadCommand({ + Bucket: this.bucket, + Key: remotePath, + UploadId: uploadID, + }), + ); + + throw error; + } + } + + async saveResultDetails( + resultID: string, + resultDetails: ResultDetails, + size: number, + ): Promise { + const metaData = { + resultID, + createdAt: new Date().toISOString(), + project: resultDetails?.project ?? "", + ...resultDetails, + size: bytesToString(size), + sizeBytes: size, + }; + + await this.write(RESULTS_BUCKET, [ + { + name: `${resultID}.json`, + content: JSON.stringify(metaData), + }, + ]); + + return metaData as Result; + } + + private async uploadReport( + reportId: string, + reportPath: string, + remotePath: string, + ) { + console.log(`[s3] upload report: ${reportPath}`); + + const files = await fs.readdir(reportPath, { + recursive: true, + withFileTypes: true, + }); + + await processBatch(this, files, this.batchSize, async (file) => { + if (!file.isFile()) { + return; + } + + console.log(`[s3] uploading file: ${JSON.stringify(file)}`); + + const nestedPath = (file as any).path.split(reportId).pop(); + const s3Path = path.join(remotePath, nestedPath ?? "", file.name); + + console.log(`[s3] uploading to ${s3Path}`); + + const { error } = await withError( + this.uploadFileWithRetry( + s3Path, + path.join((file as any).path, file.name), + ), + ); + + if (error) { + console.error(`[s3] failed to upload report: ${error.message}`); + throw new Error(`[s3] failed to upload report: ${error.message}`); + } + }); + } + + private async uploadFileWithRetry( + remotePath: string, + filePath: string, + attempt = 1, + ): Promise { + if (attempt > 3) { + throw new Error( + `[s3] failed to upload file after ${attempt} attempts: ${filePath}`, + ); + } + + const fileStream = createReadStream(filePath); + + const { error } = await withError( + this.client.send( + new PutObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + Body: fileStream, + }), + ), + ); + + if (error) { + console.error(`[s3] failed to upload file: ${error.message}`); + console.log(`[s3] will retry in 3s...`); + + return await this.uploadFileWithRetry(remotePath, filePath, attempt + 1); + } + } + + private async clearTempFolders(id?: string) { + const withReportPathMaybe = id ? ` for report ${id}` : ""; + + console.log(`[s3] clear temp folders${withReportPathMaybe}`); + + await withError( + fs.rm(path.join(TMP_FOLDER, id ?? ""), { recursive: true, force: true }), + ); + await withError(fs.rm(REPORTS_FOLDER, { recursive: true, force: true })); + } + + async generateReport( + resultsIds: string[], + metadata?: ReportMetadata, + ): Promise<{ reportId: UUID; reportPath: string }> { + console.log( + `[s3] generate report from results: ${JSON.stringify(resultsIds)}`, + ); + console.log(`[s3] create temp folders`); + const { error: mkdirReportsError } = await withError( + fs.mkdir(REPORTS_FOLDER, { recursive: true }), + ); + + if (mkdirReportsError) { + console.error( + `[s3] failed to create reports folder: ${mkdirReportsError.message}`, + ); + } + + const reportId = randomUUID(); + const tempFolder = path.join(TMP_FOLDER, reportId); + + const { error: mkdirTempError } = await withError( + fs.mkdir(tempFolder, { recursive: true }), + ); + + if (mkdirTempError) { + console.error( + `[s3] failed to create temporary folder: ${mkdirTempError.message}`, + ); + } + + console.log(`[s3] start processing...`); + + for (const resultId of resultsIds) { + const fileName = `${resultId}.zip`; + const objectKey = path.join(RESULTS_BUCKET, fileName); + + console.log(`[s3] checking existence of result: ${objectKey}`); + const { error: headError } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ), + ); + + if (headError) { + console.error( + `[s3] result ${resultId} not found, skipping: ${headError.message}`, + ); + throw new Error(`failed to check ${objectKey}: ${headError.message}`); + } + + console.log(`[s3] downloading result: ${objectKey}`); + const localFilePath = path.join(tempFolder, fileName); + + const { error: downloadError } = await withError( + (async () => { + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: objectKey, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localFilePath); + + return new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + })(), + ); + + if (downloadError) { + console.error( + `[s3] failed to download ${objectKey}: ${downloadError.message}`, + ); + + throw new Error( + `failed to download ${objectKey}: ${downloadError.message}`, + ); + } + + console.log(`[s3] downloaded: ${objectKey} to ${localFilePath}`); + } + + const { reportPath } = await generatePlaywrightReport(reportId, metadata!); + + console.log(`[s3] report generated: ${reportId} | ${reportPath}`); + + const { result: info, error: parseReportMetadataError } = await withError( + this.parseReportMetadata(reportId, reportPath, metadata), + ); + + if (parseReportMetadataError) + console.error(parseReportMetadataError.message); + + const remotePath = path.join( + REPORTS_BUCKET, + metadata?.project ?? "", + reportId, + ); + + const { error: uploadError } = await withError( + this.uploadReport(reportId, reportPath, remotePath), + ); + + if (uploadError) { + console.error(`[s3] failed to upload report: ${uploadError.message}`); + } else { + const { error } = await withError( + this.saveReportMetadata(reportId, reportPath, info ?? metadata ?? {}), + ); + + if (error) + console.error(`[s3] failed to save report metadata: ${error.message}`); + } + + await this.clearTempFolders(reportId); + + return { reportId, reportPath }; + } + + private async saveReportMetadata( + reportId: string, + reportPath: string, + metadata: ReportMetadata, + ) { + console.log( + `[s3] report uploaded: ${reportId}, uploading metadata to ${reportPath}`, + ); + const { error: metadataError } = await withError( + this.write(path.join(REPORTS_BUCKET, metadata.project ?? "", reportId), [ + { + name: REPORT_METADATA_FILE, + content: JSON.stringify(metadata), + }, + ]), + ); + + if (metadataError) + console.error( + `[s3] failed to upload report metadata: ${metadataError.message}`, + ); + } + + private async parseReportMetadata( + reportId: string, + reportPath: string, + metadata?: Record, + htmlContent?: string, // to pass file content if stored on s3 + ): Promise { + console.log( + `[s3] creating report metadata for ${reportId} and ${reportPath}`, + ); + const html = + htmlContent ?? + (await fs.readFile(path.join(reportPath, "index.html"), "utf-8")); + + const info = await parse(html as string); + + const content = Object.assign(info, metadata, { + reportId, + createdAt: new Date().toISOString(), + }); + + return content; + } + + async readConfigFile(): Promise<{ + result?: SiteWhiteLabelConfig; + error: Error | null; + }> { + await this.ensureBucketExist(); + console.log(`[s3] checking config file`); + + const { result: response, error } = await withError( + this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: APP_CONFIG_S3, + }), + ), + ); + + if (error) { + console.error(`[s3] failed to read config file: ${error.message}`); + + return { error }; + } + + const stream = response?.Body as Readable; + let existingConfig = ""; + + for await (const chunk of stream ?? []) { + existingConfig += chunk.toString(); + } + + try { + const parsed = JSON.parse(existingConfig); + + const isValid = isConfigValid(parsed); + + if (!isValid) { + return { error: new Error("invalid config") }; + } + + // ensure custom images available locally in data folder + for (const image of [ + { path: parsed.faviconPath, default: defaultConfig.faviconPath }, + { path: parsed.logoPath, default: defaultConfig.logoPath }, + ]) { + if (!image) continue; + if (image.path === image.default) continue; + + const localPath = path.join(DATA_FOLDER, image.path); + const { error: accessError } = await withError(fs.access(localPath)); + + if (accessError) { + const remotePath = path.join(DATA_PATH, image.path); + + console.log( + `[s3] downloading config image: ${remotePath} to ${localPath}`, + ); + + const response = await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: remotePath, + }), + ); + + const stream = response.Body as Readable; + const writeStream = createWriteStream(localPath); + + await new Promise((resolve, reject) => { + stream.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + stream.on("error", reject); + }); + } + } + + return { result: parsed, error: null }; + } catch (e) { + return { + error: new Error( + `failed to parse config: ${e instanceof Error ? e.message : e}`, + ), + }; + } + } + + async saveConfigFile(config: Partial) { + console.log(`[s3] writing config file`); + + const { result: existingConfig, error: readExistingConfigError } = + await this.readConfigFile(); + + if (readExistingConfigError) { + console.error( + `[s3] failed to read existing config file: ${readExistingConfigError.message}`, + ); + } + + const { error: clearExistingConfigError } = await withError( + this.clear(APP_CONFIG_S3), + ); + + if (clearExistingConfigError) { + console.error( + `[s3] failed to clear existing config file: ${clearExistingConfigError.message}`, + ); + } + + const uploadConfig = { + ...(existingConfig ?? {}), + ...config, + } as SiteWhiteLabelConfig; + + const isDefaultImage = (key: keyof SiteWhiteLabelConfig) => + config[key] && config[key] === defaultConfig[key]; + + const shouldBeUploaded = async (key: keyof SiteWhiteLabelConfig) => { + if (!config[key]) return false; + if (isDefaultImage(key)) return false; + + const imagePath = + key === "logoPath" ? uploadConfig.logoPath : uploadConfig.faviconPath; + + const { result } = await withError( + this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: path.join(DATA_PATH, imagePath), + }), + ), + ); + + if (!result) { + return true; + } + + return false; + }; + + if (await shouldBeUploaded("logoPath")) { + await this.uploadConfigImage(uploadConfig.logoPath); + } + + if (await shouldBeUploaded("faviconPath")) { + await this.uploadConfigImage(uploadConfig.faviconPath); + } + + const { error } = await withError( + this.write(DATA_PATH, [ + { + name: "config.json", + content: JSON.stringify(uploadConfig, null, 2), + }, + ]), + ); + + if (error) + console.error(`[s3] failed to write config file: ${error.message}`); + + return { result: uploadConfig, error }; + } + + private async uploadConfigImage(imagePath: string): Promise { + console.log(`[s3] uploading config image: ${imagePath}`); + + const localPath = path.join(DATA_FOLDER, imagePath); + const remotePath = path.join(DATA_PATH, imagePath); + + const { error } = await withError( + this.uploadFileWithRetry(remotePath, localPath), + ); + + if (error) { + console.error(`[s3] failed to upload config image: ${error.message}`); + + return error; + } + + return null; + } +} diff --git a/backend/src/lib/storage/types.ts b/backend/src/lib/storage/types.ts new file mode 100644 index 00000000..857103e4 --- /dev/null +++ b/backend/src/lib/storage/types.ts @@ -0,0 +1,121 @@ +import type { PassThrough } from "node:stream"; +import type { ReportInfo, ReportTest } from "@/lib/parser/types"; + +import type { SiteWhiteLabelConfig, UUID } from "@/types"; +import type { Pagination } from "./pagination"; + +export interface Storage { + getServerDataInfo: () => Promise; + readFile: ( + targetPath: string, + contentType: string | null, + ) => Promise; + readResults: () => Promise; + readReports: () => Promise; + readReport: ( + reportID: string, + reportPath: string, + ) => Promise; + deleteResults: (resultIDs: string[]) => Promise; + deleteReports: (reports: ReportPath[]) => Promise; + saveResult: (filename: string, stream: PassThrough) => Promise; + saveResultDetails: ( + resultID: string, + resultDetails: ResultDetails, + size: number, + ) => Promise; + generateReport: ( + resultsIds: string[], + metadata?: ReportMetadata, + ) => Promise<{ reportId: UUID; reportPath: string }>; + readConfigFile: () => Promise<{ + result?: SiteWhiteLabelConfig; + error: Error | null; + }>; + saveConfigFile: ( + config: Partial, + ) => Promise<{ result: SiteWhiteLabelConfig; error: Error | null }>; +} + +export interface ReportPath { + reportID: string; + project: string; +} + +export interface ReadResultsInput { + pagination?: Pagination; + project?: string; + testRun?: string; + tags?: string[]; + search?: string; +} + +export interface ReadResultsOutput { + results: Result[]; + total: number; +} + +export interface ReadReportsInput { + pagination?: Pagination; + project?: string; + ids?: string[]; + search?: string; +} + +export interface ReadReportsOutput { + reports: ReportHistory[]; + total: number; +} + +export interface ReadReportsHistory { + reports: ReportHistory[]; + total: number; +} + +// For custom user fields +export interface ResultDetails { + [key: string]: string; +} + +export type Result = { + resultID: UUID; + title?: string; + createdAt: string; + project: string; + size: string; + sizeBytes: number; +} & ResultDetails; + +export type Report = { + reportID: string; + title?: string; + project: string; + reportUrl: string; + createdAt: Date; + size: string; + sizeBytes: number; +}; + +export type ReportHistory = Report & ReportInfo; + +export const isReportHistory = ( + report: Report | ReportHistory | undefined, +): report is ReportHistory => + !!report && typeof report === "object" && "stats" in report; + +export type TestHistory = Report & ReportTest; + +export type ReportMetadata = Partial<{ + title: string; + project: string; + playwrightVersion?: string; +}> & + Record; + +export interface ServerDataInfo { + dataFolderSizeinMB: string; + numOfResults: number; + resultsFolderSizeinMB: string; + numOfReports: number; + reportsFolderSizeinMB: string; +} diff --git a/backend/src/lib/tailwind.ts b/backend/src/lib/tailwind.ts new file mode 100644 index 00000000..df1f8ec9 --- /dev/null +++ b/backend/src/lib/tailwind.ts @@ -0,0 +1,52 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +import { ReportTestOutcome } from "./parser"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +enum OutcomeColors { + Success = "success", + Primary = "primary", + Danger = "danger", + Warning = "warning", + Default = "default", +} + +export function testStatusToColor(outcome?: ReportTestOutcome): { + title: string; + colorName: OutcomeColors; + color: string; +} { + const outcomes = { + [ReportTestOutcome.Expected]: { + title: "Passed", + colorName: OutcomeColors.Success, + color: "text-success-500", + }, + [ReportTestOutcome.Unexpected]: { + title: "Failed", + colorName: OutcomeColors.Danger, + color: "text-danger-500", + }, + [ReportTestOutcome.Flaky]: { + title: "Flaky", + colorName: OutcomeColors.Warning, + color: "text-warning-500", + }, + [ReportTestOutcome.Skipped]: { + title: "Skipped", + colorName: OutcomeColors.Default, + color: "text-gray-500", + }, + unknown: { + title: "N/A", + colorName: OutcomeColors.Primary, + color: "text-gray-200", + }, + }; + + return outcome ? outcomes[outcome] : outcomes.unknown; +} diff --git a/backend/src/lib/time.ts b/backend/src/lib/time.ts new file mode 100644 index 00000000..b905b5a4 --- /dev/null +++ b/backend/src/lib/time.ts @@ -0,0 +1,27 @@ +export const parseMilliseconds = (ms: number) => { + const seconds = Math.floor(ms / 1000); + const leftMs = ms % 1000; + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s ${leftMs}ms`; + } + + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s ${leftMs}ms`; + } + + if (seconds > 0) { + return `${seconds}s ${leftMs}ms`; + } + + return `${leftMs}ms`; +}; + +export const getTimestamp = (date?: Date | string) => { + if (!date) return 0; + if (typeof date === "string") return new Date(date).getTime(); + + return date.getTime(); +}; diff --git a/backend/src/lib/transformers.ts b/backend/src/lib/transformers.ts new file mode 100644 index 00000000..7f46cfe1 --- /dev/null +++ b/backend/src/lib/transformers.ts @@ -0,0 +1,68 @@ +import type { ReportFile, ReportTest, ReportTestFilters } from "@/lib/parser"; +import type { ReportHistory } from "@/lib/storage/types"; + +const isTestMatchingFilters = ( + test: ReportTest, + filters?: ReportTestFilters, +): boolean => { + if (!filters) { + return true; + } + const byOutcome = + !filters.outcomes || filters.outcomes.includes(test.outcome); + const byTitle = + !filters.name || + test.title.toLowerCase().includes(filters.name.toLowerCase()); + + return byOutcome && byTitle; +}; + +export const filterReportHistory = ( + report: ReportHistory, + filters?: ReportTestFilters, +): ReportHistory & { testCount: number; totalTestCount: number } => { + const filtered = structuredClone(report); + const counter = { + testCount: 0, + totalTestCount: 0, + }; + const filteredFiles = filtered.files.reduce((files, file) => { + counter.totalTestCount += file.tests.length; + const filteredTests = file.tests.filter((test) => + isTestMatchingFilters(test, filters), + ); + + counter.testCount += filteredTests.length; + + const fileHasTests = filteredTests.length > 0; + + if (!fileHasTests) { + return files; + } + + file.tests = filteredTests; + + files.push(file); + + return files; + }, [] as ReportFile[]); + + filtered.files = filteredFiles; + + return { + ...filtered, + ...counter, + }; +}; + +export const pluralize = ( + count: number, + singular: string, + plural: string, + locale: string = "en-US", +): string => { + const pluralRules = new Intl.PluralRules(locale); + const rule = pluralRules.select(count); + + return rule === "one" ? singular : plural; +}; diff --git a/backend/src/lib/url.ts b/backend/src/lib/url.ts new file mode 100644 index 00000000..0226a39c --- /dev/null +++ b/backend/src/lib/url.ts @@ -0,0 +1,8 @@ +import { env } from "../config/env"; + +export const withBase = (p = "") => { + const base = (env.API_BASE_PATH || "").replace(/\/+$/, ""); + const path = p.startsWith("/") ? p : `/${p}`; + + return `${base}${path}`; +}; diff --git a/backend/src/lib/validation/index.ts b/backend/src/lib/validation/index.ts new file mode 100644 index 00000000..de295553 --- /dev/null +++ b/backend/src/lib/validation/index.ts @@ -0,0 +1,85 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { z } from "zod"; + +export class ValidationError extends Error { + constructor( + message: string, + public details?: any, + ) { + super(message); + this.name = "ValidationError"; + } +} + +export function validateSchema( + schema: T, + data: unknown, +): z.infer { + const result = schema.safeParse(data); + if (!result.success) { + console.log("Validation error data:", data); + console.log("Validation error:", result.error); + const errorMessages = result.error.issues.map((err: any) => ({ + field: err.path.join("."), + message: err.message, + })); + throw new ValidationError("Validation failed", errorMessages); + } + return result.data; +} + +export function validateQuery(schema: T) { + return async (request: FastifyRequest, reply: FastifyReply) => { + try { + request.query = validateSchema(schema, request.query); + } catch (error) { + if (error instanceof ValidationError) { + return reply.status(400).send({ + error: "Invalid query parameters", + details: error.details, + }); + } + return reply.status(500).send({ error: "Internal server error" }); + } + }; +} + +export function validateBody(schema: T) { + return async (request: FastifyRequest, reply: FastifyReply) => { + try { + request.body = validateSchema(schema, request.body); + } catch (error) { + if (error instanceof ValidationError) { + return reply.status(400).send({ + error: "Invalid request body", + details: error.details, + }); + } + return reply.status(500).send({ error: "Internal server error" }); + } + }; +} + +export function validateParams(schema: T) { + return async (request: FastifyRequest, reply: FastifyReply) => { + try { + request.params = validateSchema(schema, request.params); + } catch (error) { + if (error instanceof ValidationError) { + return reply.status(400).send({ + error: "Invalid URL parameters", + details: error.details, + }); + } + return reply.status(500).send({ error: "Internal server error" }); + } + }; +} + +export function createJsonSchema(zodSchema: z.ZodType) { + return zodTypeToJsonSchema(zodSchema); +} + +function zodTypeToJsonSchema(_zodType: any): any { + return { type: "any" }; +} diff --git a/backend/src/lib/withError.ts b/backend/src/lib/withError.ts new file mode 100644 index 00000000..ba06b165 --- /dev/null +++ b/backend/src/lib/withError.ts @@ -0,0 +1,14 @@ +export async function withError( + promise: Promise, +): Promise<{ result: T | null; error: Error | null }> { + try { + const result = await promise; + + return { result, error: null }; + } catch (error) { + return { + result: null, + error: error instanceof Error ? error : new Error(String(error)), + }; + } +} diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 00000000..ebe6d5c5 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,239 @@ +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import jwt from "jsonwebtoken"; +import { env } from "../config/env.js"; + +const useAuth = !!env.API_TOKEN; + +// strictly recommended to specify via env var +// Use a stable default secret when AUTH_SECRET is not set to avoid JWT decryption errors +// This is only acceptable when auth is disabled (no API_TOKEN) +const secret = env.AUTH_SECRET ?? "default-secret-for-non-auth-mode"; + +const expirationHours = env.UI_AUTH_EXPIRE_HOURS + ? Number.parseInt(env.UI_AUTH_EXPIRE_HOURS, 10) + : 2; +const expirationSeconds = expirationHours * 60 * 60; + +interface AuthUser { + apiToken: string; + jwtToken: string; +} + +interface AuthRequest extends Omit { + user?: AuthUser; +} + +const createAuthTokens = (apiToken: string): AuthUser => { + const jwtToken = jwt.sign({ authorized: true }, secret); + return { apiToken, jwtToken }; +}; + +const createNoAuthTokens = (): AuthUser => { + const token = jwt.sign({ authorized: true }, secret); + return { apiToken: token, jwtToken: token }; +}; + +const verifyToken = (token: string): { authorized: boolean } | null => { + try { + return jwt.verify(token, secret) as { authorized: boolean }; + } catch (_error) { + return null; + } +}; + +export const authenticate = async ( + request: AuthRequest, + reply: FastifyReply, +) => { + if (!useAuth) { + request.user = createNoAuthTokens(); + return; + } + + const authHeader = request.headers.authorization; + const cookieToken = request.cookies.token; + + let token = null; + + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.substring(7); + } else if (cookieToken) { + token = cookieToken; + } + + if (!token) { + return reply.status(401).send({ error: "Unauthorized: No token provided" }); + } + + const decoded = verifyToken(token); + if (!decoded) { + return reply.status(401).send({ error: "Unauthorized: Invalid token" }); + } + + request.user = { + apiToken: "", // should not store the original API token in JWT + jwtToken: token, + }; +}; + +export const getSession = async (request: AuthRequest, reply: FastifyReply) => { + if (!useAuth) { + const noAuthUser = createNoAuthTokens(); + return { + user: { + apiToken: noAuthUser.apiToken, + jwtToken: noAuthUser.jwtToken, + }, + expires: new Date(Date.now() + expirationSeconds * 1000).toISOString(), + }; + } + + try { + await authenticate(request, reply); + + if (request.user) { + return { + user: { + apiToken: request.user.apiToken, + jwtToken: request.user.jwtToken, + }, + expires: new Date(Date.now() + expirationSeconds * 1000).toISOString(), + }; + } + } catch (error) { + return reply.status(401).send({ error: "Unauthorized" }); + } +}; + +export async function registerAuthRoutes(fastify: FastifyInstance) { + fastify.post("/api/auth/signin", async (request, reply) => { + try { + const { apiToken } = request.body as { apiToken?: string }; + + if (!useAuth) { + const noAuthUser = createNoAuthTokens(); + + // Set token in cookie + reply.setCookie("token", noAuthUser.jwtToken, { + path: "/", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: expirationSeconds, + }); + + return { + user: { + apiToken: noAuthUser.apiToken, + jwtToken: noAuthUser.jwtToken, + }, + success: true, + }; + } + + if (!apiToken || apiToken !== env.API_TOKEN) { + return reply.status(401).send({ + error: "Invalid API token", + success: false, + }); + } + + const authUser = createAuthTokens(apiToken); + + reply.setCookie("token", authUser.jwtToken, { + path: "/", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: expirationSeconds, + }); + + return { + user: { + apiToken: authUser.apiToken, + jwtToken: authUser.jwtToken, + }, + success: true, + }; + } catch (error) { + fastify.log.error({ error }, "Sign in error"); + return reply.status(500).send({ + error: "Internal server error", + success: false, + }); + } + }); + + fastify.post("/api/auth/signout", (_, reply) => { + try { + reply.clearCookie("token", { path: "/" }); + + return { + success: true, + message: "Signed out successfully", + }; + } catch (error) { + fastify.log.error({ error }, "Sign out error"); + return reply.status(500).send({ + error: "Internal server error", + success: false, + }); + } + }); + + fastify.get("/api/auth/session", async (request, reply) => { + try { + const sessionData = await getSession(request as AuthRequest, reply); + return sessionData; + } catch (error) { + return reply.status(401).send({ error: "Unauthorized" }); + } + }); + + fastify.get("/api/auth/csrf", async (_request, _reply) => { + const csrfToken = Buffer.from(Date.now().toString()).toString("base64"); + + return { + csrfToken, + }; + }); + + fastify.get("/api/auth/providers", async (_request, _reply) => { + return { + credentials: { + id: "credentials", + name: "API Token", + type: "credentials", + signinUrl: "/api/auth/signin/credentials", + callbackUrl: "/api/auth/callback/credentials", + }, + }; + }); + + fastify.all("/api/auth/*", async (request, reply) => { + // Handle various next-auth endpoints for compatibility + const path = (request.params as { "*": string })["*"] || ""; + + switch (path) { + case "signin": + return reply.send({ + providers: [ + { + id: "credentials", + name: "API Token", + type: "credentials", + }, + ], + }); + + case "session": + return await getSession(request as AuthRequest, reply); + + default: + return reply.status(404).send({ error: "Not found" }); + } + }); +} + +export type { AuthRequest, AuthUser }; +export { useAuth, secret, expirationSeconds }; diff --git a/backend/src/routes/config.ts b/backend/src/routes/config.ts new file mode 100644 index 00000000..ee38a8a2 --- /dev/null +++ b/backend/src/routes/config.ts @@ -0,0 +1,276 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { env } from "../config/env.js"; +import { cronService } from "../lib/service/cron.js"; +import { getDatabaseStats } from "../lib/service/db/index.js"; +import { service } from "../lib/service/index.js"; +import { JiraService } from "../lib/service/jira.js"; +import { DATA_FOLDER } from "../lib/storage/constants.js"; +import { withError } from "../lib/withError.js"; +import type { SiteWhiteLabelConfig } from "../types/index.js"; +import { type AuthRequest, authenticate } from "./auth.js"; + +interface MultipartFile { + fieldname: string; + filename?: string; + toBuffer(): Promise; +} + +interface ConfigFormData { + title?: string; + logoPath?: string; + faviconPath?: string; + reporterPaths?: string; + headerLinks?: string; + jiraBaseUrl?: string; + jiraEmail?: string; + jiraApiToken?: string; + jiraProjectKey?: string; + resultExpireDays?: string; + resultExpireCronSchedule?: string; + reportExpireDays?: string; + reportExpireCronSchedule?: string; +} + +export async function registerConfigRoutes(fastify: FastifyInstance) { + fastify.get("/api/config", async (_request, reply) => { + const { result: config, error } = await withError(service.getConfig()); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + const envInfo = { + authRequired: !!env.API_TOKEN, + database: getDatabaseStats(), + dataStorage: env.DATA_STORAGE, + s3Endpoint: env.S3_ENDPOINT, + s3Bucket: env.S3_BUCKET, + }; + + return { ...config, ...envInfo }; + }); + + fastify.patch( + "/api/config", + async (request: FastifyRequest, reply: FastifyReply) => { + try { + const authResult = await authenticate(request as AuthRequest, reply); + if (authResult) return authResult; + + const data = await request.file({ + limits: { files: 2 }, + }); + + if (!data) { + return reply.status(400).send({ error: "No data received" }); + } + + let logoFile: MultipartFile | null = null; + let faviconFile: MultipartFile | null = null; + const formData: ConfigFormData = {}; + + for await (const part of data.file) { + if (part.type === "file") { + const filePart = part as MultipartFile; + if (part.fieldname === "logo") { + logoFile = filePart; + } else if (part.fieldname === "favicon") { + faviconFile = filePart; + } + } else if (part.type === "field") { + const fieldName = part.fieldname as keyof ConfigFormData; + formData[fieldName] = part.value as string; + } + } + + const config = await service.getConfig(); + + if (!config) { + return reply.status(500).send({ error: "failed to get config" }); + } + + if (logoFile) { + const { error: logoError } = await withError( + writeFile( + join(DATA_FOLDER, logoFile.filename!), + Buffer.from(await logoFile.toBuffer()), + ), + ); + + if (logoError) { + return reply + .status(500) + .send({ error: `failed to save logo: ${logoError?.message}` }); + } + config.logoPath = `/${logoFile.filename}`; + } + + if (faviconFile) { + const { error: faviconError } = await withError( + writeFile( + join(DATA_FOLDER, faviconFile.filename!), + Buffer.from(await faviconFile.toBuffer()), + ), + ); + + if (faviconError) { + return reply.status(500).send({ + error: `failed to save favicon: ${faviconError?.message}`, + }); + } + config.faviconPath = `/${faviconFile.filename}`; + } + + if (formData.title !== undefined) { + config.title = formData.title; + } + + if (formData.logoPath !== undefined && !logoFile) { + config.logoPath = formData.logoPath; + } + + if (formData.faviconPath !== undefined && !faviconFile) { + config.faviconPath = formData.faviconPath; + } + + if (formData.reporterPaths !== undefined) { + try { + config.reporterPaths = JSON.parse(formData.reporterPaths); + } catch { + config.reporterPaths = [formData.reporterPaths]; + } + } + + if (formData.headerLinks !== undefined) { + try { + const parsedHeaderLinks = JSON.parse(formData.headerLinks); + if (parsedHeaderLinks) config.headerLinks = parsedHeaderLinks; + } catch (error) { + return reply.status(400).send({ + error: `failed to parse header links: ${error instanceof Error ? error.message : "Invalid JSON"}`, + }); + } + } + + if (!config.jira) { + config.jira = {}; + } + + if (formData.jiraBaseUrl !== undefined) + config.jira.baseUrl = formData.jiraBaseUrl; + if (formData.jiraEmail !== undefined) + config.jira.email = formData.jiraEmail; + if (formData.jiraApiToken !== undefined) + config.jira.apiToken = formData.jiraApiToken; + if (formData.jiraProjectKey !== undefined) + config.jira.projectKey = formData.jiraProjectKey; + + if ( + formData.jiraBaseUrl || + formData.jiraEmail || + formData.jiraApiToken || + formData.jiraProjectKey + ) { + JiraService.resetInstance(); + } + + if (!config.cron) { + config.cron = {}; + } + + if (formData.resultExpireDays !== undefined) { + config.cron.resultExpireDays = Number.parseInt( + formData.resultExpireDays, + 10, + ); + } + if (formData.resultExpireCronSchedule !== undefined) { + config.cron.resultExpireCronSchedule = + formData.resultExpireCronSchedule; + } + if (formData.reportExpireDays !== undefined) { + config.cron.reportExpireDays = Number.parseInt( + formData.reportExpireDays, + 10, + ); + } + if (formData.reportExpireCronSchedule !== undefined) { + config.cron.reportExpireCronSchedule = + formData.reportExpireCronSchedule; + } + + const { error: saveConfigError } = await withError( + service.updateConfig(config), + ); + + if (saveConfigError) { + return reply.status(500).send({ + error: `failed to save config: ${saveConfigError.message}`, + }); + } + + if ( + config.cron?.resultExpireDays || + config.cron?.resultExpireCronSchedule || + config.cron?.reportExpireDays || + config.cron?.reportExpireCronSchedule + ) { + await cronService.restart(); + } + + return reply.send({ message: "config saved" }); + } catch (error) { + fastify.log.error({ error }, "Config update error"); + return reply.status(400).send({ + error: `config update failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + } + }, + ); + + fastify.post("/api/config", async (request, reply) => { + const { result, error } = await withError( + service.updateConfig(request.body as Partial), + ); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return result; + }); + + fastify.get("/api/info", async (_request, reply) => { + const { result: info, error } = await withError(service.getServerInfo()); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return info; + }); + + fastify.post( + "/api/cache/refresh", + { + schema: { + body: { type: "object", additionalProperties: true }, + response: { + 200: { type: "object" }, + 400: { type: "object", properties: { error: { type: "string" } } }, + }, + }, + }, + async (_request, reply) => { + const { result, error } = await withError(service.refreshCache()); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return result; + }, + ); +} diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts new file mode 100644 index 00000000..faf7fa7b --- /dev/null +++ b/backend/src/routes/index.ts @@ -0,0 +1,16 @@ +import type { FastifyInstance } from "fastify"; +import { registerAuthRoutes } from "./auth.js"; +import { registerConfigRoutes } from "./config.js"; +import { registerJiraRoutes } from "./jira.js"; +import { registerReportRoutes } from "./reports.js"; +import { registerResultRoutes } from "./results.js"; +import { registerServeRoutes } from "./serve.js"; + +export async function registerApiRoutes(fastify: FastifyInstance) { + await registerAuthRoutes(fastify); + await registerReportRoutes(fastify); + await registerResultRoutes(fastify); + await registerConfigRoutes(fastify); + await registerServeRoutes(fastify); + await registerJiraRoutes(fastify); +} diff --git a/backend/src/routes/jira.ts b/backend/src/routes/jira.ts new file mode 100644 index 00000000..d899910a --- /dev/null +++ b/backend/src/routes/jira.ts @@ -0,0 +1,173 @@ +import type { FastifyInstance } from "fastify"; +import { service } from "../lib/service/index.js"; +import { JiraService } from "../lib/service/jira.js"; +import { withError } from "../lib/withError.js"; +import { type AuthRequest, authenticate } from "./auth.js"; + +interface CreateTicketRequest { + summary: string; + description: string; + issueType: string; + projectKey: string; + testId: string; + testTitle: string; + testOutcome: string; + testLocation: { + file: string; + line: number; + column: number; + }; + reportId: string; + testAttachments?: Array<{ + name: string; + path: string; + contentType: string; + }>; +} + +export async function registerJiraRoutes(fastify: FastifyInstance) { + fastify.get("/api/jira/config", async (_request, reply) => { + try { + const config = await service.getConfig(); + const jiraConfig = config.jira; + + const isConfigured = !!( + jiraConfig?.baseUrl && + jiraConfig?.email && + jiraConfig?.apiToken + ); + + if (!isConfigured) { + return reply.send({ + configured: false, + message: + "Jira is not configured. Please configure Jira settings in the admin panel.", + config: jiraConfig || {}, + }); + } + + const jiraService = JiraService.getInstance(jiraConfig); + + let issueTypes = []; + + if (jiraConfig?.projectKey) { + try { + const project = await jiraService.getProject(jiraConfig.projectKey); + + issueTypes = project.issueTypes || []; + } catch (error) { + fastify.log.warn( + { error }, + `Could not fetch project-specific issue types for ${jiraConfig.projectKey}`, + ); + } + } + + return reply.send({ + configured: true, + baseUrl: jiraConfig.baseUrl, + defaultProjectKey: jiraConfig.projectKey, + issueTypes: issueTypes.map((type: any) => ({ + id: type.id, + name: type.name, + description: type.description, + })), + }); + } catch (error) { + return reply.status(500).send({ + configured: false, + error: `Failed to connect to Jira: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + } + }); + + fastify.post("/api/jira/create-ticket", async (request, reply) => { + const authResult = await authenticate(request as AuthRequest, reply); + if (authResult) return authResult; + + const { result: data, error: parseError } = await withError( + Promise.resolve(request.body), + ); + + if (parseError) { + return reply.status(400).send({ error: parseError.message }); + } + + if (!data) { + return reply.status(400).send({ error: "Request data is missing" }); + } + + const ticketData = data as CreateTicketRequest; + + try { + const report = await service.getReport(ticketData.reportId); + const projectPath = report.project ? `${report.project}/` : ""; + + ticketData.testAttachments = ticketData.testAttachments?.map((att) => ({ + ...att, + path: `${projectPath}${ticketData.reportId}/${att.path}`, + })); + } catch (error) { + fastify.log.error( + { error }, + `Failed to get report ${ticketData.reportId}`, + ); + } + + try { + if (!ticketData.summary || !ticketData.projectKey) { + return reply.status(400).send({ + error: "Summary and project key are required", + }); + } + + const config = await service.getConfig(); + const jiraService = JiraService.getInstance(config.jira); + + const jiraResponse = await jiraService.createIssue( + ticketData.summary, + ticketData.description, + ticketData.issueType, + ticketData.projectKey, + { + testId: ticketData.testId, + testTitle: ticketData.testTitle, + testOutcome: ticketData.testOutcome, + testLocation: ticketData.testLocation, + }, + ticketData.testAttachments, + ); + + return reply.status(201).send({ + success: true, + issueKey: jiraResponse.key, + issueId: jiraResponse.id, + issueUrl: jiraResponse.self, + message: "Jira ticket created successfully", + data: { + ...ticketData, + issueKey: jiraResponse.key, + issueId: jiraResponse.id, + issueUrl: jiraResponse.self, + created: new Date().toISOString(), + }, + }); + } catch (error) { + fastify.log.error({ error }, "Failed to create Jira ticket"); + + if ( + error instanceof Error && + error.message.includes("Jira configuration is incomplete") + ) { + return reply.status(500).send({ + error: + "Jira is not configured. Please set up JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.", + }); + } + + return reply.status(500).send({ + error: `Failed to create Jira ticket: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + } + }); +} diff --git a/backend/src/routes/reports.ts b/backend/src/routes/reports.ts new file mode 100644 index 00000000..49b3ed91 --- /dev/null +++ b/backend/src/routes/reports.ts @@ -0,0 +1,186 @@ +import type { FastifyInstance } from "fastify"; +import { + DeleteReportsRequestSchema, + GenerateReportRequestSchema, + GetReportParamsSchema, + ListReportsQuerySchema, +} from "../lib/schemas/index.js"; +import { service } from "../lib/service/index.js"; +import { parseFromRequest } from "../lib/storage/pagination.js"; +import { validateSchema } from "../lib/validation/index.js"; +import { withError } from "../lib/withError.js"; + +export async function registerReportRoutes(fastify: FastifyInstance) { + fastify.get("/api/report/list", async (request, reply) => { + try { + const query = validateSchema(ListReportsQuerySchema, request.query); + const params = new URLSearchParams(); + if (query.limit !== undefined) { + params.append("limit", query.limit.toString()); + } + if (query.offset !== undefined) { + params.append("offset", query.offset.toString()); + } + const pagination = parseFromRequest(params); + + const { result: reports, error } = await withError( + service.getReports({ + pagination, + project: query.project, + search: query.search, + }), + ); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return reports; + } catch (error) { + console.error("[routes] list reports error:", error); + return reply.status(500).send({ error: "Internal server error" }); + } + }); + + fastify.get("/api/report/:id", async (request, reply) => { + try { + const params = validateSchema(GetReportParamsSchema, request.params); + const { result: report, error } = await withError( + service.getReport(params.id), + ); + + if (error) { + return reply.status(404).send({ error: error.message }); + } + + return report; + } catch (error) { + console.error("[routes] get report error:", error); + return reply.status(500).send({ error: "Internal server error" }); + } + }); + + fastify.get("/api/report/projects", async (_request, reply) => { + try { + const { result: projects, error } = await withError( + service.getReportsProjects(), + ); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return projects; + } catch (error) { + console.error("[routes] get projects error:", error); + return reply.status(500).send({ error: "Internal server error" }); + } + }); + + fastify.post("/api/report/generate", async (request, reply) => { + try { + const body = request.body as { resultsIds?: unknown; [key: string]: unknown } || {}; + + if (!body.resultsIds || !Array.isArray(body.resultsIds)) { + return reply + .status(400) + .send({ error: "resultsIds array is required" }); + } + + if (body.resultsIds.length === 0) { + return reply + .status(400) + .send({ error: "At least one result ID must be provided" }); + } + + const validatedBody = validateSchema(GenerateReportRequestSchema, body); + console.log(`[routes] generate report request:`, validatedBody); + + const metadata: Record = { + ...(validatedBody.project && { project: validatedBody.project }), + ...(validatedBody.playwrightVersion && { + playwrightVersion: validatedBody.playwrightVersion, + }), + ...(validatedBody.title && { title: validatedBody.title }), + ...Object.fromEntries( + Object.entries(validatedBody) + .filter( + ([key]) => + ![ + "resultsIds", + "project", + "playwrightVersion", + "title", + ].includes(key) && + typeof validatedBody[key as keyof typeof validatedBody] === + "string", + ) + .map(([key, value]) => [key, String(value)]), + ), + }; + + const { result, error } = await withError( + service.generateReport(validatedBody.resultsIds, metadata), + ); + + if (error) { + console.error(`[routes] generate report error:`, error.message); + + if ( + error instanceof Error && + error.message.includes("ENOENT: no such file or directory") + ) { + return reply.status(404).send({ + error: `ResultID not found: ${error.message}`, + }); + } + + return reply.status(400).send({ error: error.message }); + } + + console.log(`[routes] generate report success:`, result); + return result; + } catch (error) { + console.error("[routes] generate report validation error:", error); + return reply.status(400).send({ error: "Invalid request format" }); + } + }); + + fastify.delete("/api/report/delete", async (request, reply) => { + try { + const body = request.body as { reportsIds?: unknown } || { reportsIds: [] }; + + if (!body.reportsIds || !Array.isArray(body.reportsIds)) { + return reply + .status(400) + .send({ error: "reportsIds array is required" }); + } + + if (body.reportsIds.length === 0) { + return reply + .status(400) + .send({ error: "At least one report ID must be provided" }); + } + + const validatedBody = validateSchema(DeleteReportsRequestSchema, body); + console.log(`[routes] delete reports:`, validatedBody.reportsIds); + + const { error } = await withError( + service.deleteReports(validatedBody.reportsIds), + ); + + if (error) { + console.error(`[routes] delete reports error:`, error); + return reply.status(404).send({ error: error.message }); + } + + return reply.status(200).send({ + message: "Reports deleted successfully", + reportsIds: validatedBody.reportsIds, + }); + } catch (error) { + console.error("[routes] delete reports validation error:", error); + return reply.status(400).send({ error: "Invalid request format" }); + } + }); +} diff --git a/backend/src/routes/results.ts b/backend/src/routes/results.ts new file mode 100644 index 00000000..81b3fd19 --- /dev/null +++ b/backend/src/routes/results.ts @@ -0,0 +1,328 @@ +import { randomUUID } from "node:crypto"; +import { PassThrough } from "node:stream"; +import type { FastifyInstance } from "fastify"; +import { + DeleteResultsRequestSchema, + ListResultsQuerySchema, +} from "../lib/schemas/index.js"; +import { service } from "../lib/service/index.js"; +import { DEFAULT_STREAM_CHUNK_SIZE } from "../lib/storage/constants.js"; +import { parseFromRequest } from "../lib/storage/pagination.js"; +import { validateSchema } from "../lib/validation/index.js"; +import { withError } from "../lib/withError.js"; +import { type AuthRequest, authenticate } from "./auth.js"; + +export async function registerResultRoutes(fastify: FastifyInstance) { + fastify.get("/api/result/list", async (request, reply) => { + try { + const query = validateSchema(ListResultsQuerySchema, request.query); + const params = new URLSearchParams(); + if (query.limit !== undefined) { + params.append("limit", query.limit.toString()); + } + if (query.offset !== undefined) { + params.append("offset", query.offset.toString()); + } + const pagination = parseFromRequest(params); + const tags = query.tags ? query.tags.split(",").filter(Boolean) : []; + + const { result, error } = await withError( + service.getResults({ + pagination, + project: query.project, + tags, + search: query.search, + }), + ); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return result; + } catch (error) { + console.error("[routes] list results error:", error); + return reply.status(500).send({ error: "Internal server error" }); + } + }); + + fastify.get("/api/result/projects", async (_, reply) => { + const { result: projects, error } = await withError( + service.getResultsProjects(), + ); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return projects; + }); + + fastify.get("/api/result/tags", async (_, reply) => { + const { result: tags, error } = await withError(service.getResultsTags()); + + if (error) { + return reply.status(400).send({ error: error.message }); + } + + return tags; + }); + + fastify.delete( + "/api/result/delete", + { + config: { + rawBody: true, + }, + }, + async (request, reply) => { + try { + const body = request.body as { resultsIds?: unknown } || { resultsIds: [] }; + + if (!body.resultsIds || !Array.isArray(body.resultsIds)) { + return reply + .status(400) + .send({ error: "resultsIds array is required" }); + } + + if (body.resultsIds.length === 0) { + return reply + .status(400) + .send({ error: "At least one result ID must be provided" }); + } + + const validatedBody = validateSchema(DeleteResultsRequestSchema, body); + console.log(`[routes] delete results:`, validatedBody.resultsIds); + + const { error } = await withError( + service.deleteResults(validatedBody.resultsIds), + ); + + if (error) { + console.error(`[routes] delete results error:`, error); + return reply.status(404).send({ error: error.message }); + } + + console.log(`[routes] delete results - deletion successful`); + return reply.status(200).send({ + message: "Results files deleted successfully", + resultsIds: validatedBody.resultsIds, + }); + } catch (error) { + console.error("[routes] delete results validation error:", error); + return reply.status(400).send({ error: "Invalid request format" }); + } + }, + ); + + fastify.put("/api/result/upload", async (request, reply) => { + const authResult = await authenticate(request as AuthRequest, reply); + if (authResult) return authResult; + + const resultID = randomUUID(); + const fileName = `${resultID}.zip`; + + const query = request.query as Record; + const contentLength = query["fileContentLength"] || ""; + + if (contentLength || Number.parseInt(contentLength, 10)) { + console.log( + `[upload] fileContentLength query parameter is provided for result ${resultID}, using presigned URL flow`, + ); + } + + // if there is fileContentLength query parameter we can use presigned URL for direct upload + const presignedUrl = contentLength + ? await service.getPresignedUrl(fileName) + : ""; + + const resultDetails: Record = {}; + let fileSize = 0; + + const filePassThrough = new PassThrough({ + highWaterMark: DEFAULT_STREAM_CHUNK_SIZE, + }); + + try { + const data = await request.file({ + limits: { files: 1, fileSize: 100 * 1024 * 1024 }, + }); + + if (!data) { + return reply + .status(400) + .send({ error: "upload result failed: No file received" }); + } + + for (const [key, prop] of Object.entries(data.fields)) { + if (key === "file") continue; + + if (prop && typeof prop === "object" && "value" in prop) { + resultDetails[key] = String(prop.value); + } else { + resultDetails[key] = typeof prop === "string" ? prop : String(prop); + } + } + + if (data.file && !Array.isArray(data.file)) { + let saveResultPromise: Promise; + + saveResultPromise = service.saveResult( + fileName, + filePassThrough, + presignedUrl, + contentLength, + ); + + let isPaused = false; + + data.file.on("data", (chunk: Buffer) => { + fileSize += chunk.length; + + const canContinue = filePassThrough.write(chunk); + + if (!canContinue && !isPaused) { + isPaused = true; + data.file?.pause(); + } + }); + + filePassThrough.on("drain", () => { + if (isPaused) { + isPaused = false; + data.file?.resume(); + } + }); + + data.file.on("end", () => { + console.log("[upload] file ended"); + filePassThrough.end(); + }); + + data.file.on("error", (error) => { + console.error("[upload] file error:", error); + filePassThrough.destroy(); + throw error; + }); + + data.file.pipe(filePassThrough); + await saveResultPromise; + + if (contentLength) { + const expected = Number.parseInt(contentLength, 10); + if ( + Number.isFinite(expected) && + expected > 0 && + fileSize !== expected + ) { + return reply.status(400).send({ + error: `Size mismatch: received ${fileSize} bytes, expected ${expected} bytes`, + }); + } + } + } + + const { result: uploadResult, error: uploadResultDetailsError } = + await withError( + service.saveResultDetails(resultID, resultDetails, fileSize), + ); + + if (uploadResultDetailsError) { + return reply.status(400).send({ + error: `upload result details failed: ${uploadResultDetailsError.message}`, + }); + } + + let generatedReport = null; + + if ( + resultDetails.shardCurrent && + resultDetails.shardTotal && + resultDetails.triggerReportGeneration === "true" + ) { + const { result: results, error: resultsError } = await withError( + service.getResults({ + testRun: resultDetails.testRun, + }), + ); + + if (resultsError) { + return reply.status(500).send({ + error: `failed to generate report: ${resultsError.message}`, + }); + } + + const testRunResults = results?.results.filter( + (result) => + result.testRun === resultDetails.testRun && + (resultDetails.project + ? result.project === resultDetails.project + : true), + ); + + console.log( + `found ${testRunResults?.length} results for the test run ${resultDetails.testRun}`, + ); + + if ( + testRunResults?.length === Number.parseInt(resultDetails.shardTotal) + ) { + const ids = testRunResults.map((result) => result.resultID); + + console.log( + "triggerReportGeneration for", + resultDetails.testRun, + ids, + ); + const { result, error } = await withError( + service.generateReport(ids, { + project: resultDetails.project, + testRun: resultDetails.testRun, + playwrightVersion: resultDetails.playwrightVersion, + }), + ); + + if (error) { + return reply + .status(500) + .send({ error: `failed to generate report: ${error.message}` }); + } + + generatedReport = result; + } + } + + return reply.status(200).send({ + message: "Success", + data: { + ...uploadResult, + generatedReport, + }, + }); + } catch (error) { + console.error("[upload] error:", error); + + if (!filePassThrough.destroyed) { + filePassThrough.destroy(); + } + + const { error: deleteError } = await withError( + service.deleteResults([resultID]), + ); + if (deleteError) { + console.error( + `[upload] cleanup failed for result ${resultID}:`, + deleteError, + ); + reply.status(400).send({ + error: `upload result failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + return; + } + + return reply.status(400).send({ + error: `upload result failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }); + } + }); +} diff --git a/backend/src/routes/serve.ts b/backend/src/routes/serve.ts new file mode 100644 index 00000000..7784765b --- /dev/null +++ b/backend/src/routes/serve.ts @@ -0,0 +1,83 @@ +import { access, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { FastifyInstance } from "fastify"; +import mime from "mime"; +import { env } from "../config/env.js"; +import { DATA_FOLDER } from "../lib/storage/constants.js"; +import { storage } from "../lib/storage/index.js"; +import { withError } from "../lib/withError.js"; +import { type AuthRequest, authenticate } from "./auth.js"; + +export async function registerServeRoutes(fastify: FastifyInstance) { + fastify.get("/api/serve/*", async (request, reply) => { + try { + const filePath = (request.params as { "*": string })["*"] || ""; + const targetPath = decodeURI(filePath); + + const authRequired = !!env.API_TOKEN; + + if (authRequired) { + const authResult = await authenticate(request as AuthRequest, reply); + if (authResult) return authResult; + } + + const contentType = mime.getType(targetPath.split("/").pop() || ""); + + if (!contentType && !targetPath.includes(".")) { + return reply.code(404).send({ error: "Not Found" }); + } + + const { result: content, error } = await withError( + storage.readFile(targetPath, contentType || null), + ); + + if (error || !content) { + return reply.code(404).send({ + error: `Could not read file: ${error?.message || "File not found"}`, + }); + } + + const headers: Record = { + "Content-Type": contentType ?? "application/octet-stream", + }; + + if ((request as AuthRequest).user?.apiToken) { + headers["X-API-Token"] = (request as AuthRequest).user!.apiToken; + } + + return reply.code(200).headers(headers).send(content); + } catch (error) { + fastify.log.error({ error }, "File serving error"); + return reply.code(500).send({ error: "Internal server error" }); + } + }); + + fastify.get("/api/static/*", async (request, reply) => { + try { + const filePath = (request.params as { "*": string })["*"] || ""; + const targetPath = decodeURI(filePath); + + const contentType = mime.getType(targetPath.split("/").pop() || ""); + + if (!contentType && !targetPath.includes(".")) { + return reply.code(404).send({ error: "Not Found" }); + } + + const imageDataPath = join(DATA_FOLDER, targetPath); + const imagePublicPath = join(process.cwd(), "public", targetPath); + + const { error: dataAccessError } = await withError(access(imageDataPath)); + const imagePath = dataAccessError ? imagePublicPath : imageDataPath; + + const imageBuffer = await readFile(imagePath); + + return reply + .code(200) + .header("Content-Type", contentType || "image/*") + .send(imageBuffer); + } catch (error) { + fastify.log.error({ error }, "Static file serving error"); + return reply.code(404).send({ error: "File not found" }); + } + }); +} diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts new file mode 100644 index 00000000..ceed7d77 --- /dev/null +++ b/backend/src/types/index.ts @@ -0,0 +1,45 @@ +import type { HeaderLinks } from "../config/site.js"; + +export type UUID = `${string}-${string}-${string}-${string}-${string}`; + +export interface JiraConfig { + baseUrl?: string; + email?: string; + apiToken?: string; + projectKey?: string; +} + +export interface SiteWhiteLabelConfig { + title: string; + headerLinks: HeaderLinks; + logoPath: string; + faviconPath: string; + reporterPaths?: string[]; + authRequired?: boolean; + database?: DatabaseStats; + dataStorage?: string; + s3Endpoint?: string; + s3Bucket?: string; + cron?: { + resultExpireDays?: number; + resultExpireCronSchedule?: string; + reportExpireDays?: number; + reportExpireCronSchedule?: string; + }; + jira?: JiraConfig; +} + +export interface DatabaseStats { + sizeOnDisk: string; + estimatedRAM: string; + reports: number; + results: number; +} + +export interface EnvInfo { + authRequired: boolean; + database: DatabaseStats; + dataStorage: string | undefined; + s3Endpoint: string | undefined; + s3Bucket: string | undefined; +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 00000000..4fdd547d --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/backend/tsconfig.node.json b/backend/tsconfig.node.json new file mode 100644 index 00000000..eca66688 --- /dev/null +++ b/backend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/backend/vite.config.ts b/backend/vite.config.ts new file mode 100644 index 00000000..63b20dd9 --- /dev/null +++ b/backend/vite.config.ts @@ -0,0 +1,10 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vite"; + +export default defineConfig({ + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, +}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..b673d13e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + + Playwright Reports Server + + + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..29c00bfb --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,8151 @@ +{ + "name": "playwright-reports-server-frontend", + "version": "5.7.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright-reports-server-frontend", + "version": "5.7.4", + "dependencies": { + "@heroui/link": "2.2.12", + "@heroui/navbar": "2.2.13", + "@heroui/react": "2.7.4", + "@heroui/switch": "2.2.13", + "@heroui/system": "2.4.11", + "@heroui/theme": "2.4.11", + "@react-aria/selection": "^3.12.1", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.15.0", + "@react-aria/visually-hidden": "^3.8.28", + "@tanstack/react-query": "^5.59.15", + "@tanstack/react-query-devtools": "^5.59.15", + "clsx": "^2.1.1", + "framer-motion": "^11.11.8", + "next-themes": "^0.4.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.0.2", + "recharts": "^2.13.0", + "sonner": "^1.5.0", + "tailwind-merge": "^3.0.2" + }, + "devDependencies": { + "@types/react": "18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "10.4.20", + "postcss": "8.4.47", + "tailwind-variants": "0.2.1", + "tailwindcss": "3.4.13", + "typescript": "5.2.2", + "vite": "^6.0.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@heroui/accordion": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.12.tgz", + "integrity": "sha512-ged/4UkDgro/6yL8vN7tdgMYcClq1ttA3dpF8xfqhoThS3P8yA9I/rX4Bf6WB1V/QOPnDpat6N9nJj5mKTwUGA==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/divider": "2.2.10", + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-accordion": "2.2.7", + "@react-aria/button": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-stately/tree": "3.8.7", + "@react-types/accordion": "3.0.0-alpha.26", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/accordion/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/alert": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@heroui/alert/-/alert-2.2.15.tgz", + "integrity": "sha512-Zvwilzf5iYiL6aKz5FV2yzjImBi3jE0NYXLjLR7h1zczUSewKaUG0aqYCo8mw3U9SWX7Nj2DP1mboLyNCD0mRA==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.15", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@react-aria/utils": "3.27.0", + "@react-stately/utils": "3.10.5" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/alert/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/aria-utils": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/aria-utils/-/aria-utils-2.2.12.tgz", + "integrity": "sha512-vTcIo0NE6htNY6vSltmmwgQH5Lye+6/dBWcFG18qmBptiwFjJ6EPxoHzBNtUkRY2wJFYNNKOfPViQBe9qQepwA==", + "license": "MIT", + "dependencies": { + "@heroui/react-rsc-utils": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/system": "2.4.11", + "@react-aria/utils": "3.27.0", + "@react-stately/collections": "3.12.1", + "@react-stately/overlays": "3.6.13", + "@react-types/overlays": "3.8.12", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/aria-utils/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/autocomplete": { + "version": "2.3.16", + "resolved": "https://registry.npmjs.org/@heroui/autocomplete/-/autocomplete-2.3.16.tgz", + "integrity": "sha512-brOUl5cuIaWgftOffFGi1IG4LijKRCvMGgWL8MNylI1xrsBfl1VmeRuSn8CpJDKllZICseePOTerLKITl/50Kg==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/button": "2.2.15", + "@heroui/form": "2.1.14", + "@heroui/input": "2.4.15", + "@heroui/listbox": "2.3.14", + "@heroui/popover": "2.3.15", + "@heroui/react-utils": "2.1.8", + "@heroui/scroll-shadow": "2.3.10", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/spinner": "2.2.12", + "@heroui/use-aria-button": "2.2.9", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/combobox": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/combobox": "3.10.2", + "@react-types/combobox": "3.13.2", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/autocomplete/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/autocomplete/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/avatar": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/avatar/-/avatar-2.2.11.tgz", + "integrity": "sha512-exXfXsma3X33YoU3C7n96zctGRtOn8yIsIF97quTekSThWn4bcCCo2Otu2hLbGZVR5QikyPWlG6MnXTohzqXxw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-image": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/avatar/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/badge": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@heroui/badge/-/badge-2.2.10.tgz", + "integrity": "sha512-ZY+7zvgHUW7Ye4Epdd4GnbmJgf59pGjxGML6Jm+R+GvjdqUSpneRW+QUNzwL8paSwXbGTP48nnQh9fMs+PKyRQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/breadcrumbs": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/breadcrumbs/-/breadcrumbs-2.2.11.tgz", + "integrity": "sha512-DfrJhUT+HyiFh/F5j6CI7yxVuwd+F2sfJOC3EJrrbsrUS1q5ZXapkX1e4hkzAJ4SG31cb+fM3Np+8ga/Vdq+sg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@react-aria/breadcrumbs": "3.5.20", + "@react-aria/focus": "3.19.1", + "@react-aria/utils": "3.27.0", + "@react-types/breadcrumbs": "3.7.10", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/breadcrumbs/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/button": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@heroui/button/-/button-2.2.15.tgz", + "integrity": "sha512-ixKxTiwHBMz0vVur4vKGBplOwJwZx0gg1fb6Rzs79CivXu/pgZQkGj9SyC3smkJ+PNRMHxKnMg5haMTZCiINpw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/ripple": "2.2.12", + "@heroui/shared-utils": "2.1.7", + "@heroui/spinner": "2.2.12", + "@heroui/use-aria-button": "2.2.9", + "@react-aria/button": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-types/button": "3.10.2", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/button/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/calendar": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/@heroui/calendar/-/calendar-2.2.15.tgz", + "integrity": "sha512-B8qfMwp6ijc7rY6SrN4XxBHGYCJodmAs6wF51R8H7Cmn+2YW4nCbjl+thf8RdE8oA/f130rvew43cdJPjHcy7A==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.15", + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-button": "2.2.9", + "@internationalized/date": "3.7.0", + "@react-aria/calendar": "3.7.0", + "@react-aria/focus": "3.19.1", + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/calendar": "3.7.0", + "@react-stately/utils": "3.10.5", + "@react-types/button": "3.10.2", + "@react-types/calendar": "3.6.0", + "@react-types/shared": "3.27.0", + "@types/lodash.debounce": "^4.0.7", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/calendar/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/calendar/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/card": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@heroui/card/-/card-2.2.14.tgz", + "integrity": "sha512-s8ZaCCSChkGehvLGYX3VEDX70PzOwQAG6ikXaEeHD3poO7wMZzdxQw73RtCGf/XAlJCpWgJ18xTdHGfm3aQESQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/ripple": "2.2.12", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-button": "2.2.9", + "@react-aria/button": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/card/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/checkbox": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@heroui/checkbox/-/checkbox-2.3.14.tgz", + "integrity": "sha512-Yn1h6IhYlhxEsR235BUgHzh6yAR6JPTzdr97PSKf/xJox0D4HK6uyKj0YCmPwIgYCxGfd1NaqXRTm3FM4RRkFg==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-callback-ref": "2.1.6", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/checkbox": "3.15.1", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/checkbox": "3.6.11", + "@react-stately/toggle": "3.8.1", + "@react-types/checkbox": "3.9.1", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.3", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/checkbox/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/checkbox/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/chip": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/chip/-/chip-2.2.11.tgz", + "integrity": "sha512-FFSK+bwnbZFXA2zk/Y49VKvwZlgMCEk6L0du/4knyFc1vtoI2+vr+W8uRM82xXAXptCkrZlXKm6vlDKaKTDH2Q==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-types/checkbox": "3.9.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/chip/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/code": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/code/-/code-2.2.11.tgz", + "integrity": "sha512-nNz8wxpm5epPWptVca1sWKpqYvCEGXVWzuuTLcXLX4lmmZyE/Dj0G3rlf1Sq8skxzQpdfPy52mvUog+/ONrJfQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/system-rsc": "2.3.10" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/date-input": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@heroui/date-input/-/date-input-2.3.14.tgz", + "integrity": "sha512-p7qgD5BAMuJGQcXMUbRdMG0tXRPZtrLiF/sMysVHUYsxLTAdi99j+7a1l905GhLHSsHuqdWoGr/YfAxVzytftg==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@internationalized/date": "3.7.0", + "@react-aria/datepicker": "3.13.0", + "@react-aria/i18n": "3.12.5", + "@react-aria/utils": "3.27.0", + "@react-stately/datepicker": "3.12.0", + "@react-types/datepicker": "3.10.0", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/date-input/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/date-picker": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@heroui/date-picker/-/date-picker-2.3.15.tgz", + "integrity": "sha512-MLzeMB1//WClwKF6IAuEcHChiobd4SVa0jr5Y7ScYzj4Gw9qvDAd/cfIZUrhQ0nXHZUvwmfcLEi2YdVqMuFWZQ==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/button": "2.2.15", + "@heroui/calendar": "2.2.15", + "@heroui/date-input": "2.3.14", + "@heroui/form": "2.1.14", + "@heroui/popover": "2.3.15", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@internationalized/date": "3.7.0", + "@react-aria/datepicker": "3.13.0", + "@react-aria/i18n": "3.12.5", + "@react-aria/utils": "3.27.0", + "@react-stately/datepicker": "3.12.0", + "@react-stately/overlays": "3.6.13", + "@react-stately/utils": "3.10.5", + "@react-types/datepicker": "3.10.0", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/date-picker/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/divider": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@heroui/divider/-/divider-2.2.10.tgz", + "integrity": "sha512-L3aEQ+ClUafzFJCmgCPjWL3ZDVHqSPRjLRE0XHpqC2xAP5dU0Hq44lhzCYHHKHHk1ZP/f1WDeas65C8lcHLZpA==", + "license": "MIT", + "dependencies": { + "@heroui/react-rsc-utils": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/system-rsc": "2.3.10", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/dom-animation": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/dom-animation/-/dom-animation-2.1.6.tgz", + "integrity": "sha512-l4xh+y02lmoJVdLR0cjpsa7LjLIvVQCX+w+S2KW6tOoPKmHlyW/8r7h6SqPB4Ua1NZGmRHtlYmw+mw47yqyTjw==", + "license": "MIT", + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1" + } + }, + "node_modules/@heroui/drawer": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/drawer/-/drawer-2.2.12.tgz", + "integrity": "sha512-27OEnfn82S2b143C372yKx8DBUct5htbk6KFHlH/enZeMsppOLnSCXsX7QpZas8c6B5k03otvUyL+YQ29/JdTw==", + "license": "MIT", + "dependencies": { + "@heroui/framer-utils": "2.1.11", + "@heroui/modal": "2.2.12", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/dropdown": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@heroui/dropdown/-/dropdown-2.3.15.tgz", + "integrity": "sha512-MoOFQJy8MBL5Qiclirt5LouEYi9+4H4wlrAdCYdsGGrdoivS2UT/jllc2oEwRLYyRLgnkKyqh5UqnxHWNrnX6g==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/menu": "2.2.14", + "@heroui/popover": "2.3.15", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/menu": "3.17.0", + "@react-aria/utils": "3.27.0", + "@react-stately/menu": "3.9.1", + "@react-types/menu": "3.9.14" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/dropdown/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/form": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@heroui/form/-/form-2.1.14.tgz", + "integrity": "sha512-4OZ8dqekSkXca1hbhU1gUBEdrVm6QHafrzbyTpDHaohwE+ea+OuWKn5ttlS/S5wamcnxTErmG3uyvsZGsDspBw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/system": "2.4.11", + "@heroui/theme": "2.4.11", + "@react-aria/utils": "3.27.0", + "@react-stately/form": "3.1.1", + "@react-types/form": "3.7.9", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@heroui/form/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/framer-utils": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@heroui/framer-utils/-/framer-utils-2.1.11.tgz", + "integrity": "sha512-E8MLdLKvIVlAEVVeivORivt0HzN9y9LunmcOok3BhrBhSI0yBVBMrgd4XZJzFsNipgu2L687nxHbWeUs8jK/5g==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7", + "@heroui/system": "2.4.11", + "@heroui/use-measure": "2.1.6" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/image": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@heroui/image/-/image-2.2.10.tgz", + "integrity": "sha512-02v0bJShCwaoXAjfMLGV900HsJ4J5YtW3OHJD/TIGWQzHNYxv7Mls4u2PyfUpk6IDimlZ+fIiEjfV0zR/HY3MA==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-image": "2.1.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/input": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@heroui/input/-/input-2.4.15.tgz", + "integrity": "sha512-ImnbIQ6h6gHgYTho1l+t+rzEehl86HJsshycoAfCi0Pn5U9KAe0Xv/ePZ6PeVhCC25QUcv1Ey5G2AkgobF49tg==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/textfield": "3.16.0", + "@react-aria/utils": "3.27.0", + "@react-stately/utils": "3.10.5", + "@react-types/shared": "3.27.0", + "@react-types/textfield": "3.11.0", + "react-textarea-autosize": "^8.5.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/input-otp": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@heroui/input-otp/-/input-otp-2.1.14.tgz", + "integrity": "sha512-3Nd3+6zRxxIX6rSwdAoCm1eIqsyLqGjnFQhVG55FCyeWllfl3OQEwe3QZuQvpeBUVudeqBFCu7m2+Wbkv+r83g==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/form": "3.0.12", + "@react-aria/utils": "3.27.0", + "@react-stately/form": "3.1.1", + "@react-stately/utils": "3.10.5", + "@react-types/textfield": "3.11.0", + "input-otp": "1.4.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@heroui/input-otp/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/input/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/kbd": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/kbd/-/kbd-2.2.11.tgz", + "integrity": "sha512-yM1B4lxW2tgptKDt9rdw7T/HDEHGZLoSvB9V6X+0vJ/AjQMFZcqAIrYEUkM8BupX+Mokrur7dc036Mo3pYk5ww==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/system-rsc": "2.3.10", + "@react-aria/utils": "3.27.0" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/kbd/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/link": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/link/-/link-2.2.12.tgz", + "integrity": "sha512-LQTD9xSwEosgMHjZJfc0kN3AmBJfRv8W7LRuevHanSwusYvyreHGRoESIjPnTJ1+KNC01SuJlHt3K0y+JCkSfg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-link": "2.2.10", + "@react-aria/focus": "3.19.1", + "@react-aria/link": "3.7.8", + "@react-aria/utils": "3.27.0", + "@react-types/link": "3.5.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/link/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/listbox": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@heroui/listbox/-/listbox-2.3.14.tgz", + "integrity": "sha512-PANgexYzI7L9em6F/va0TzFrnPOLMgSWKgUTcmdAGrOefWnD7CwxyFeduiNNNDZ1m/WmLzWPwjPLDTxahHIfpw==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/divider": "2.2.10", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-is-mobile": "2.2.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/listbox": "3.14.0", + "@react-aria/utils": "3.27.0", + "@react-stately/list": "3.11.2", + "@react-types/menu": "3.9.14", + "@react-types/shared": "3.27.0", + "@tanstack/react-virtual": "3.11.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/listbox/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/menu": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@heroui/menu/-/menu-2.2.14.tgz", + "integrity": "sha512-0CqyB2tsL/6ftxMShndyFHZySIUuqJHy4i3v7CP6NbnYv7oL6HVRB3ysg6qVaiXILF5S8HTTJNU1/kYvVgjfUQ==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/divider": "2.2.10", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-is-mobile": "2.2.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/menu": "3.17.0", + "@react-aria/utils": "3.27.0", + "@react-stately/menu": "3.9.1", + "@react-stately/tree": "3.8.7", + "@react-types/menu": "3.9.14", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/menu/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/modal": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/modal/-/modal-2.2.12.tgz", + "integrity": "sha512-kWoghgH/oihaBv0PzkfLbKDXWEZRVUHUEaxHCTI5Ut/5rAwxR/tYW9Wy+UW6m5nshefsq+RYh5cD9iPvDv4T1A==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-button": "2.2.9", + "@heroui/use-aria-modal-overlay": "2.2.8", + "@heroui/use-disclosure": "2.2.7", + "@heroui/use-draggable": "2.1.7", + "@react-aria/dialog": "3.5.21", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-stately/overlays": "3.6.13", + "@react-types/overlays": "3.8.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/modal/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/navbar": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@heroui/navbar/-/navbar-2.2.13.tgz", + "integrity": "sha512-gXg0j0rDxKG2SGZF/8x62O6K+yKcMXgVYx1Bs7yCvOO0Nc3PtOfkShxZqCMxY9d5qYHEI2hq3QvxjcDVXIoypg==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-scroll-position": "2.1.6", + "@react-aria/button": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-stately/toggle": "3.8.1", + "@react-stately/utils": "3.10.5" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/navbar/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/number-input": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@heroui/number-input/-/number-input-2.0.5.tgz", + "integrity": "sha512-GgvF3jPYzYG2fNpVYDC2wFjKRdJbM987enHzMheK2PaRo0Kf8nNaxVQZX/ZFJY1KVJeAHvQ/QqSDEHBmDp+5rQ==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.15", + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/focus": "3.19.1", + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/numberfield": "3.11.10", + "@react-aria/utils": "3.27.0", + "@react-stately/numberfield": "3.9.9", + "@react-stately/utils": "3.10.5", + "@react-types/button": "3.10.2", + "@react-types/numberfield": "3.8.8", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/number-input/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/pagination": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@heroui/pagination/-/pagination-2.2.13.tgz", + "integrity": "sha512-TJankzcEb9unzvVKL6nWbvxD4U1RPhDZlkO26LylIRW7/bLHbeehJeY0a2yHxIIVjtiaNTyQK2MV9PUXdtKfrg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-intersection-observer": "2.2.7", + "@heroui/use-pagination": "2.2.8", + "@react-aria/focus": "3.19.1", + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/pagination/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/popover": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@heroui/popover/-/popover-2.3.15.tgz", + "integrity": "sha512-KPLRB5L5a185pyCjyFDanGfVb8x/jni4wNpZB0OaPmOHj1/uIDACkmtavIqxIHu8IzrRGnAO1c0nt6iNg2qjfA==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/button": "2.2.15", + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-aria-button": "2.2.9", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/dialog": "3.5.21", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-stately/overlays": "3.6.13", + "@react-types/button": "3.10.2", + "@react-types/overlays": "3.8.12" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/popover/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/progress": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/progress/-/progress-2.2.11.tgz", + "integrity": "sha512-y7GtCxqkCyZr4tifDOS4/XAxgBH6+PqKOmuPC9pHCRL7b8VFeKM9KFkxjS6n6VM/zdKqnmlysdHDFtIi3ppVBw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-is-mounted": "2.1.6", + "@react-aria/i18n": "3.12.5", + "@react-aria/progress": "3.4.19", + "@react-aria/utils": "3.27.0", + "@react-types/progress": "3.5.9" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/progress/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/radio": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@heroui/radio/-/radio-2.3.14.tgz", + "integrity": "sha512-k0KSH8Q7D8pacpu9pZxobH3MJ/zg8MGvDWj0J0TY9ZpUnetSTG+YBuwTtlRGDKz8IBmu7kZ3rlym8NU6wbir2g==", + "license": "MIT", + "dependencies": { + "@heroui/form": "2.1.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/radio": "3.10.11", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/radio": "3.10.10", + "@react-types/radio": "3.8.6", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.3", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/radio/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/radio/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/react": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@heroui/react/-/react-2.7.4.tgz", + "integrity": "sha512-w5RZxxx0DHxciLRsJ2v8oPeHIc/FH1sVuWUGM9Vjg5/0TZoZNYryDtsubMJLKAe3Yu010kxY9qOdR8GA3f25eg==", + "license": "MIT", + "dependencies": { + "@heroui/accordion": "2.2.12", + "@heroui/alert": "2.2.15", + "@heroui/autocomplete": "2.3.16", + "@heroui/avatar": "2.2.11", + "@heroui/badge": "2.2.10", + "@heroui/breadcrumbs": "2.2.11", + "@heroui/button": "2.2.15", + "@heroui/calendar": "2.2.15", + "@heroui/card": "2.2.14", + "@heroui/checkbox": "2.3.14", + "@heroui/chip": "2.2.11", + "@heroui/code": "2.2.11", + "@heroui/date-input": "2.3.14", + "@heroui/date-picker": "2.3.15", + "@heroui/divider": "2.2.10", + "@heroui/drawer": "2.2.12", + "@heroui/dropdown": "2.3.15", + "@heroui/form": "2.1.14", + "@heroui/framer-utils": "2.1.11", + "@heroui/image": "2.2.10", + "@heroui/input": "2.4.15", + "@heroui/input-otp": "2.1.14", + "@heroui/kbd": "2.2.11", + "@heroui/link": "2.2.12", + "@heroui/listbox": "2.3.14", + "@heroui/menu": "2.2.14", + "@heroui/modal": "2.2.12", + "@heroui/navbar": "2.2.13", + "@heroui/number-input": "2.0.5", + "@heroui/pagination": "2.2.13", + "@heroui/popover": "2.3.15", + "@heroui/progress": "2.2.11", + "@heroui/radio": "2.3.14", + "@heroui/ripple": "2.2.12", + "@heroui/scroll-shadow": "2.3.10", + "@heroui/select": "2.4.15", + "@heroui/skeleton": "2.2.10", + "@heroui/slider": "2.4.12", + "@heroui/snippet": "2.2.16", + "@heroui/spacer": "2.2.11", + "@heroui/spinner": "2.2.12", + "@heroui/switch": "2.2.13", + "@heroui/system": "2.4.11", + "@heroui/table": "2.2.14", + "@heroui/tabs": "2.2.12", + "@heroui/theme": "2.4.11", + "@heroui/toast": "2.0.5", + "@heroui/tooltip": "2.2.12", + "@heroui/user": "2.2.11", + "@react-aria/visually-hidden": "3.8.19" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react-rsc-utils": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/react-rsc-utils/-/react-rsc-utils-2.1.6.tgz", + "integrity": "sha512-slBWi9g3HdnSNRhoedDhXFybaab5MveAeECzQoj4oJrIlmiezyeZWRKbWR8li2tiZtvBoEr0Xpu/A8hdni15dQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react-utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@heroui/react-utils/-/react-utils-2.1.8.tgz", + "integrity": "sha512-ET8sQaqfAWEviuZfatSYXBzyD0PpzuIK2YQkijla0TmF0sHJ3Yl4YQ6DYleWAaIJEWW1u0HgUPrdIjVGjWyKVg==", + "license": "MIT", + "dependencies": { + "@heroui/react-rsc-utils": "2.1.6", + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/react/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/ripple": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/ripple/-/ripple-2.2.12.tgz", + "integrity": "sha512-5hKlJfl05rtp/ABhmsJ/qqQjh9TgzyvBdeuvWf0K3PJVIMSp+LJly86mwlEzHEbbBwAJvdq9jxd3+R54ZMaQRw==", + "license": "MIT", + "dependencies": { + "@heroui/dom-animation": "2.1.6", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/scroll-shadow": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@heroui/scroll-shadow/-/scroll-shadow-2.3.10.tgz", + "integrity": "sha512-l10qKwQLWxW0l94SNxh+z8UnzgWlhTmvNRezrjXZZFhv4EKgv8u1f/E0HsLTy/g8KgPU0ebGWQmbhdqfMyiqOg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-data-scroll-overflow": "2.2.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/select": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@heroui/select/-/select-2.4.15.tgz", + "integrity": "sha512-FSbZd+M9RR/CY1hLJJ9bMQ6FbKQQS+f4jC6GGv2NAqx5kJ1wB1XjGpRRTPyZm1eKKLHc347DszOZRlYAMTxRxA==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/form": "2.1.14", + "@heroui/listbox": "2.3.14", + "@heroui/popover": "2.3.15", + "@heroui/react-utils": "2.1.8", + "@heroui/scroll-shadow": "2.3.10", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/spinner": "2.2.12", + "@heroui/use-aria-button": "2.2.9", + "@heroui/use-aria-multiselect": "2.4.8", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/focus": "3.19.1", + "@react-aria/form": "3.0.12", + "@react-aria/interactions": "3.23.0", + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-types/shared": "3.27.0", + "@tanstack/react-virtual": "3.11.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/select/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/select/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/shared-icons": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/shared-icons/-/shared-icons-2.1.6.tgz", + "integrity": "sha512-4Gey+FJF4XBlMw5p9D2geOEAED8xCxuksurWKUz7eAoAivRRsZJf9wwUsKvNfrmboBUoytdxpUDbVgnckx/G8A==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/shared-utils": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@heroui/shared-utils/-/shared-utils-2.1.7.tgz", + "integrity": "sha512-1nx7y41P+Bsca7nDC+QFajAoFhSRGvjKhdFeopMQNTvU95L42PD7B0ThjcOretvQD0Ye2TsAEQInwsSgZ6kK/g==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/@heroui/skeleton": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@heroui/skeleton/-/skeleton-2.2.10.tgz", + "integrity": "sha512-6nv+Efzi3DBrVCVTY1CC8InaiYdmztPjmw/ytjGEm1rJNpJCK9HOgKSUVuz6dncLsIsB77toMfE+2s53Yrq9Yg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/slider": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@heroui/slider/-/slider-2.4.12.tgz", + "integrity": "sha512-ArIQ73SDKX3OL0Q8DX2aeGnPSnVaALtoYzlFthI3BzN1eQeWAPbf43Np29yMadBnuPhnBKBuSZkgGS0ziRtSsg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/tooltip": "2.2.12", + "@react-aria/focus": "3.19.1", + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/slider": "3.7.15", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/slider": "3.6.1" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/slider/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/slider/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/snippet": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@heroui/snippet/-/snippet-2.2.16.tgz", + "integrity": "sha512-QbWkGto4sP8L0LX3ZEqi5+vxWBFnRvjt1/zELY/NEFZLgMxC2BsM9uHc3ivxNNWyaV6S50Ddnl3RaL11+ZUd1g==", + "license": "MIT", + "dependencies": { + "@heroui/button": "2.2.15", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/tooltip": "2.2.12", + "@heroui/use-clipboard": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/utils": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/snippet/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/spacer": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/spacer/-/spacer-2.2.11.tgz", + "integrity": "sha512-NZp0pbZ8LJcR2XaslHPHFYJSKxStN+gO8eymNIQjumSRBU0cHpNcrxbNy/N3gBtCqOrrainbmSIXxpxifWeOyA==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/system-rsc": "2.3.10" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/spinner": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/spinner/-/spinner-2.2.12.tgz", + "integrity": "sha512-P7tbxZg3E2vd2+5cpvNme1b6ytAy+BWEh7Q3JHjiIMjR70+140HulChp0nRkxIizoKF+vY5qGwEz+rAvteJ9dg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/system": "2.4.11", + "@heroui/system-rsc": "2.3.10" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/switch": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@heroui/switch/-/switch-2.2.13.tgz", + "integrity": "sha512-lPxGTZewHtSe2WfsMBoQajbr0NMfDSw8bHZWqpoe+ks+d8RCDNkULknkziFb5xzRQNfUoa6cnQ5P0vGqcG9YIw==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/switch": "3.6.11", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/toggle": "3.8.1", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.3", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/switch/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/switch/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/system": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.11.tgz", + "integrity": "sha512-6OI7bgRLaH8i8kATeB6WOOLkHoxxSbfRaJr21bb2w+tnHkeqqHU73pURUR+t8jlheP9iS/j1IqGrcjVB9HR6sg==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/system-rsc": "2.3.10", + "@internationalized/date": "3.7.0", + "@react-aria/i18n": "3.12.5", + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-stately/utils": "3.10.5", + "@react-types/datepicker": "3.10.0" + }, + "peerDependencies": { + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/system-rsc": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@heroui/system-rsc/-/system-rsc-2.3.10.tgz", + "integrity": "sha512-/feLXGslWl27fGfot3ePaku53+HMqMeKwuwvJ5z6xGl3W9Npxmy+rTIS31wd7lnVWfH1ARxyODJWcfEvp/5lgg==", + "license": "MIT", + "dependencies": { + "@react-types/shared": "3.27.0", + "clsx": "^1.2.1" + }, + "peerDependencies": { + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/system-rsc/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@heroui/system/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/table": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@heroui/table/-/table-2.2.14.tgz", + "integrity": "sha512-679UslGs8/UXp8V6ONnZIIr278jR6oKSv1XVM7+70MX2wpAieXiBcFdTvycXGRcEKQIsy+6AV4EqwTh5qizoMg==", + "license": "MIT", + "dependencies": { + "@heroui/checkbox": "2.3.14", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/spacer": "2.2.11", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/table": "3.16.1", + "@react-aria/utils": "3.27.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-stately/table": "3.13.1", + "@react-stately/virtualizer": "4.2.1", + "@react-types/grid": "3.2.11", + "@react-types/table": "3.10.4", + "@tanstack/react-virtual": "3.11.3" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/table/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/table/node_modules/@react-aria/visually-hidden": { + "version": "3.8.19", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz", + "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/tabs": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/tabs/-/tabs-2.2.12.tgz", + "integrity": "sha512-yMQCumzqbCS7YGfn/nmarBEWuNAqfGy6fz+bn3uUyHSkKc4CsiRUprzF5jJoHfOFGxQI+7XIBSOtQ262Pj5o1w==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-is-mounted": "2.1.6", + "@heroui/use-update-effect": "2.1.6", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/tabs": "3.9.9", + "@react-aria/utils": "3.27.0", + "@react-stately/tabs": "3.7.1", + "@react-types/shared": "3.27.0", + "@react-types/tabs": "3.3.12", + "scroll-into-view-if-needed": "3.0.10" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/tabs/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/theme": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.11.tgz", + "integrity": "sha512-sy17DlHCxi5qIILkVhzsez7ZsibWo5gBvZlBETp3yHtxrRwCEHqJ00x2frPAvDf2s/xiN79rtwMf5tlA4w3Ubw==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7", + "clsx": "^1.2.1", + "color": "^4.2.3", + "color2k": "^2.0.3", + "deepmerge": "4.3.1", + "flat": "^5.0.2", + "tailwind-merge": "2.5.4", + "tailwind-variants": "0.3.0" + }, + "peerDependencies": { + "tailwindcss": ">=3.4.0" + } + }, + "node_modules/@heroui/theme/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@heroui/theme/node_modules/tailwind-merge": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", + "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/@heroui/theme/node_modules/tailwind-variants": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.3.0.tgz", + "integrity": "sha512-ho2k5kn+LB1fT5XdNS3Clb96zieWxbStE9wNLK7D0AV64kdZMaYzAKo0fWl6fXLPY99ffF9oBJnIj5escEl/8A==", + "license": "MIT", + "dependencies": { + "tailwind-merge": "^2.5.4" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, + "node_modules/@heroui/toast": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@heroui/toast/-/toast-2.0.5.tgz", + "integrity": "sha512-EFSJ8F1K79q2Y9ssMQhuWWeNcDAT8HMdwsZxL3hV3MZl/ZsEFobmmCeH38gCTidm63CUiIL6zaT1/FI7F+QiKQ==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/shared-icons": "2.1.6", + "@heroui/shared-utils": "2.1.7", + "@heroui/spinner": "2.2.12", + "@heroui/use-is-mobile": "2.2.7", + "@react-aria/interactions": "3.23.0", + "@react-aria/toast": "3.0.0-beta.19", + "@react-aria/utils": "3.27.0", + "@react-stately/toast": "3.0.0-beta.7", + "@react-stately/utils": "3.10.5" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.10", + "@heroui/theme": ">=2.4.9", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/toast/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/tooltip": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@heroui/tooltip/-/tooltip-2.2.12.tgz", + "integrity": "sha512-S9Hpc+DvK+E1lKEahBDGrIImzC99cVLjtB3bkv3u0zAhkEFfTs9JM5ti4wNWANG7i7G80MZemNRXHA0LQQmCtg==", + "license": "MIT", + "dependencies": { + "@heroui/aria-utils": "2.2.12", + "@heroui/dom-animation": "2.1.6", + "@heroui/framer-utils": "2.1.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@heroui/use-safe-layout-effect": "2.1.6", + "@react-aria/interactions": "3.23.0", + "@react-aria/overlays": "3.25.0", + "@react-aria/tooltip": "3.7.11", + "@react-aria/utils": "3.27.0", + "@react-stately/tooltip": "3.5.1", + "@react-types/overlays": "3.8.12", + "@react-types/tooltip": "3.4.14" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "framer-motion": ">=11.5.6 || >=12.0.0-alpha.1", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/tooltip/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-accordion": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-accordion/-/use-aria-accordion-2.2.7.tgz", + "integrity": "sha512-UPQoroLcvylGN3rSo3r350UPCl49Ava1HRjBsozqYHvTlG3PzW3ArnzDHwYjl3eh2dCtVyxDct+H4kokR2f0Ww==", + "license": "MIT", + "dependencies": { + "@react-aria/button": "3.11.1", + "@react-aria/focus": "3.19.1", + "@react-aria/selection": "3.22.0", + "@react-aria/utils": "3.27.0", + "@react-stately/tree": "3.8.7", + "@react-types/accordion": "3.0.0-alpha.26", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-accordion/node_modules/@react-aria/selection": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.22.0.tgz", + "integrity": "sha512-XFOrK525HX2eeWeLZcZscUAs5qsuC1ZxsInDXMjvLeAaUPtQNEhUKHj3psDAl6XDU4VV1IJo0qCmFTVqTTMZSg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/selection": "^3.19.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-accordion/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-button": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-button/-/use-aria-button-2.2.9.tgz", + "integrity": "sha512-hKYT8M98XDtCygGvMdCut4LpQgIRl8tKjTp7rZklWLPu3NgX2BBw0Lu2c4cjjf0XBWXMA/bTIIX9fJsd2FD6OA==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-types/button": "3.10.2", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-button/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-link": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-link/-/use-aria-link-2.2.10.tgz", + "integrity": "sha512-W8iNzUKMAWuZ7FrNxYS6HCGZLCZy91sgcMisE3GRrdXideqwb5F2OUfEY3FOYPoWDb2+AfhuKMsVfAdECS2nXQ==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/interactions": "3.23.0", + "@react-aria/utils": "3.27.0", + "@react-types/link": "3.5.10", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-link/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-modal-overlay": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-modal-overlay/-/use-aria-modal-overlay-2.2.8.tgz", + "integrity": "sha512-QYYF1zM+hZjUPnx40yDbhynH/d+2hEoYlduP6yhBXFiAk44mwm9hkJlLobQNZeiA+sobbN7EvguCYSoyYsOGYg==", + "license": "MIT", + "dependencies": { + "@react-aria/overlays": "3.25.0", + "@react-aria/utils": "3.27.0", + "@react-stately/overlays": "3.6.13", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-modal-overlay/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-multiselect": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@heroui/use-aria-multiselect/-/use-aria-multiselect-2.4.8.tgz", + "integrity": "sha512-pvwCLpmP8SL/38EJme794TCP9wR5iQPeIctZbgncXZxT9qmDONn/bO27XFWTbWJlSf3hJ38Nm0HmRgUcat6sUw==", + "license": "MIT", + "dependencies": { + "@react-aria/i18n": "3.12.5", + "@react-aria/interactions": "3.23.0", + "@react-aria/label": "3.7.14", + "@react-aria/listbox": "3.14.0", + "@react-aria/menu": "3.17.0", + "@react-aria/selection": "3.22.0", + "@react-aria/utils": "3.27.0", + "@react-stately/form": "3.1.1", + "@react-stately/list": "3.11.2", + "@react-stately/menu": "3.9.1", + "@react-types/button": "3.10.2", + "@react-types/overlays": "3.8.12", + "@react-types/select": "3.9.9", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-aria-multiselect/node_modules/@react-aria/selection": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.22.0.tgz", + "integrity": "sha512-XFOrK525HX2eeWeLZcZscUAs5qsuC1ZxsInDXMjvLeAaUPtQNEhUKHj3psDAl6XDU4VV1IJo0qCmFTVqTTMZSg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/selection": "^3.19.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-aria-multiselect/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-callback-ref": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-callback-ref/-/use-callback-ref-2.1.6.tgz", + "integrity": "sha512-icFp4WBWTZhypBcyu+5kir7nZLtvtQq7DDvGwkTtxsGnFHgGDc6sXXcOU6AcCdoGefmsiVp5c3D3lZ2pMlGHmA==", + "license": "MIT", + "dependencies": { + "@heroui/use-safe-layout-effect": "2.1.6" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-clipboard": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@heroui/use-clipboard/-/use-clipboard-2.1.7.tgz", + "integrity": "sha512-Nt/ILhHovvYpoRjhqbbyz9sPI5xquvsSU/UuZ4qE8xFrsI8ukJo9znI1mW5eeNUlY9EOjz6HWdYU1B6QyLR3hg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-data-scroll-overflow": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@heroui/use-data-scroll-overflow/-/use-data-scroll-overflow-2.2.7.tgz", + "integrity": "sha512-+XPWShncxvPt+wSz5wXIP1GRws6mZs5QoHHG9n0agPL3eYiE0dHeEVYmfLQCopYhnnTA3HRcTkRKQ6pNR4oVQQ==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-disclosure": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@heroui/use-disclosure/-/use-disclosure-2.2.7.tgz", + "integrity": "sha512-nSv2MgoWS/7MKpZGikIT1CID0VgomSs2XTCz2M011VD89v0A9k27qW68Qmf03oWyrl6UghC1XdgJe6IIsfq72A==", + "license": "MIT", + "dependencies": { + "@heroui/use-callback-ref": "2.1.6", + "@react-aria/utils": "3.27.0", + "@react-stately/utils": "3.10.5" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-disclosure/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-draggable": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@heroui/use-draggable/-/use-draggable-2.1.7.tgz", + "integrity": "sha512-TMeUiDT1yw2sSIPardaB3/JIFD12VO1pV3N/Jf0WyiYQO82v2VoturQHbRTvjI+69SjqapucZSYM0X7henlOwg==", + "license": "MIT", + "dependencies": { + "@react-aria/interactions": "3.23.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-image": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@heroui/use-image/-/use-image-2.1.7.tgz", + "integrity": "sha512-Cno8oXNo/3YDCRnCwSuJYgdsZ7mujjVWSwlYaoYbi+rM5o9TjZYRPYHZacHMABlbY+Hew31ddYpOmyw4SrkIwA==", + "license": "MIT", + "dependencies": { + "@heroui/react-utils": "2.1.8", + "@heroui/use-safe-layout-effect": "2.1.6" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-intersection-observer": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@heroui/use-intersection-observer/-/use-intersection-observer-2.2.7.tgz", + "integrity": "sha512-efEAGWlfH9+DoShOSC/3dBmxtGDsVCwIQuV+ZBAROjbal/QOCOBKYOGpMcIBruOsAs8r4I9R432QhMxkDgOTWA==", + "license": "MIT", + "dependencies": { + "@react-aria/interactions": "3.23.0", + "@react-aria/ssr": "3.9.7", + "@react-aria/utils": "3.27.0", + "@react-types/shared": "3.27.0" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-intersection-observer/node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-intersection-observer/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-is-mobile": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@heroui/use-is-mobile/-/use-is-mobile-2.2.7.tgz", + "integrity": "sha512-aaQjvATBb09c4UzkcCaeZLqv5Sz0gtA1n07LxW+LJd2ENEYEuxNOWyO7dIAHaaYb3znX1ZxGC1h4cYLcN59nPA==", + "license": "MIT", + "dependencies": { + "@react-aria/ssr": "3.9.7" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-is-mobile/node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@heroui/use-is-mounted": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-is-mounted/-/use-is-mounted-2.1.6.tgz", + "integrity": "sha512-dnTX1PUWGhIQJxszTScHgM9XxvYIx9j8vnSJuVGaptJonZWlt50yI/WAi+oWXJ289rw7XBDJ8o38qmU5Pmq+WA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-measure": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-measure/-/use-measure-2.1.6.tgz", + "integrity": "sha512-FiN3Za6hExqU1B0d2drCm9JUFneQ1W5gyNoX0owf3aIWG98QR+LR1MOL3WBAGWtDsp4K6q8rqUKXatNxGJd/sA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-pagination": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@heroui/use-pagination/-/use-pagination-2.2.8.tgz", + "integrity": "sha512-SQNJli3A5uoTl9f5xKsgkXGG8YuNUtCaAfpb+f1xQ5/v0QEBuhttRH6njT76+wXIlijSK42b2bSzU3EKY2uGjg==", + "license": "MIT", + "dependencies": { + "@heroui/shared-utils": "2.1.7", + "@react-aria/i18n": "3.12.5" + }, + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-safe-layout-effect": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-safe-layout-effect/-/use-safe-layout-effect-2.1.6.tgz", + "integrity": "sha512-yLT6zrlcZGJX4KKenzvR6lPS42Lf/Q0Q8ErpufLSkTdX4uk/ThGB/CRwdXfP+TPFLIfjXdsgCHgZr2ZAQJaG5Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-scroll-position": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-scroll-position/-/use-scroll-position-2.1.6.tgz", + "integrity": "sha512-9ap2AIuPjJCGLt7ZZAQqSE7s9Md1lUqnmxXf6UhKH0CJowhVHIl76gtV2rMeQZ+vsjbG3d4tsX2Vw13h+HLpuA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/use-update-effect": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@heroui/use-update-effect/-/use-update-effect-2.1.6.tgz", + "integrity": "sha512-nGSaIngKPuutmQcfZgnMHGYXJDqo6sPjdIIFjb5vutEnc827Xyh5f4q8hXfo7huYYYzA1CqLaThNVFCf3qIwHg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/user": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@heroui/user/-/user-2.2.11.tgz", + "integrity": "sha512-1ZpVIr7yjdJEeFPWtvHmx0Q8WJSkKYnUd5iz0m9z8pTrrQtnfWavdwsGJ5Xw68Sjsr/ig5SEjro5Yn6l4COn8A==", + "license": "MIT", + "dependencies": { + "@heroui/avatar": "2.2.11", + "@heroui/react-utils": "2.1.8", + "@heroui/shared-utils": "2.1.7", + "@react-aria/focus": "3.19.1", + "@react-aria/utils": "3.27.0" + }, + "peerDependencies": { + "@heroui/system": ">=2.4.7", + "@heroui/theme": ">=2.4.6", + "react": ">=18 || >=19.0.0-rc.0", + "react-dom": ">=18 || >=19.0.0-rc.0" + } + }, + "node_modules/@heroui/user/node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@internationalized/date": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz", + "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/message": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.8.tgz", + "integrity": "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "intl-messageformat": "^10.1.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.7.tgz", + "integrity": "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.20.tgz", + "integrity": "sha512-xqVSSDPpQuUFpJyIXMQv8L7zumk5CeGX7qTzo4XRvqm5T9qnNAX4XpYEMdktnLrQRY/OemCBScbx7SEwr0B3Kg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.5", + "@react-aria/link": "^3.7.8", + "@react-aria/utils": "^3.27.0", + "@react-types/breadcrumbs": "^3.7.10", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/button": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.11.1.tgz", + "integrity": "sha512-NSs2HxHSSPSuYy5bN+PMJzsCNDVsbm1fZ/nrWM2WWWHTBrx9OqyrEXZVV9ebzQCN9q0nzhwpf6D42zHIivWtJA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/toolbar": "3.0.0-beta.12", + "@react-aria/utils": "^3.27.0", + "@react-stately/toggle": "^3.8.1", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/calendar": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.7.0.tgz", + "integrity": "sha512-9YUbgcox7cQgvZfQtL2BLLRsIuX4mJeclk9HkFoOsAu3RGO5HNsteah8FV54W8BMjm/bNRXIPUxtjTTP+1L6jg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/utils": "^3.27.0", + "@react-stately/calendar": "^3.7.0", + "@react-types/button": "^3.10.2", + "@react-types/calendar": "^3.6.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/checkbox": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.15.1.tgz", + "integrity": "sha512-ETgsMDZ0IZzRXy/OVlGkazm8T+PcMHoTvsxp0c+U82c8iqdITA+VJ615eBPOQh6OkkYIIn4cRn/e+69RmGzXng==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.0.12", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/toggle": "^3.10.11", + "@react-aria/utils": "^3.27.0", + "@react-stately/checkbox": "^3.6.11", + "@react-stately/form": "^3.1.1", + "@react-stately/toggle": "^3.8.1", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.11.1.tgz", + "integrity": "sha512-TTNbGhUuqxzPcJzd6hufOxuHzX0UARkw+0bl+TuCwNPQnqrcPf20EoOZvd3MHZwGq6GCP4QV+qo0uGx83RpUvA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.5", + "@react-aria/listbox": "^3.14.0", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/menu": "^3.17.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/combobox": "^3.10.2", + "@react-stately/form": "^3.1.1", + "@react-types/button": "^3.10.2", + "@react-types/combobox": "^3.13.2", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/datepicker": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.13.0.tgz", + "integrity": "sha512-TmJan65P3Vk7VDBNW5rH9Z25cAn0vk8TEtaP3boCs8wJFE+HbEuB8EqLxBFu47khtuKTEqDP3dTlUh2Vt/f7Xw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@internationalized/number": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/spinbutton": "^3.6.11", + "@react-aria/utils": "^3.27.0", + "@react-stately/datepicker": "^3.12.0", + "@react-stately/form": "^3.1.1", + "@react-types/button": "^3.10.2", + "@react-types/calendar": "^3.6.0", + "@react-types/datepicker": "^3.10.0", + "@react-types/dialog": "^3.5.15", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dialog": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.21.tgz", + "integrity": "sha512-tBsn9swBhcptJ9QIm0+ur0PVR799N6qmGguva3rUdd+gfitknFScyT08d7AoMr9AbXYdJ+2R9XNSZ3H3uIWQMw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/overlays": "^3.25.0", + "@react-aria/utils": "^3.27.0", + "@react-types/dialog": "^3.5.15", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz", + "integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/form": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.12.tgz", + "integrity": "sha512-8uvPYEd3GDyGt5NRJIzdWW1Ry5HLZq37vzRZKUW8alZ2upFMH3KJJG55L9GP59KiF6zBrYBebvI/YK1Ye1PE1g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.14.5.tgz", + "integrity": "sha512-XHw6rgjlTqc85e3zjsWo3U0EVwjN5MOYtrolCKc/lc2ItNdcY3OlMhpsU9+6jHwg/U3VCSWkGvwAz9hg7krd8Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.2", + "@react-aria/i18n": "^3.12.13", + "@react-aria/interactions": "^3.25.6", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/selection": "^3.26.0", + "@react-aria/utils": "^3.31.0", + "@react-stately/collections": "^3.12.8", + "@react-stately/grid": "^3.11.6", + "@react-stately/selection": "^3.20.6", + "@react-types/checkbox": "^3.10.2", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@internationalized/date": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", + "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-aria/focus": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", + "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-aria/i18n": { + "version": "3.12.13", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.13.tgz", + "integrity": "sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-stately/collections": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz", + "integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-types/checkbox": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.10.2.tgz", + "integrity": "sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-types/grid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.3.6.tgz", + "integrity": "sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/i18n": { + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.5.tgz", + "integrity": "sha512-ooeop2pTG94PuaHoN2OTk2hpkqVuoqgEYxRvnc1t7DVAtsskfhS/gVOTqyWGsxvwAvRi7m/CnDu6FYdeQ/bK5w==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@internationalized/message": "^3.1.6", + "@internationalized/number": "^3.6.0", + "@internationalized/string": "^3.2.5", + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz", + "integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/label": { + "version": "3.7.14", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.14.tgz", + "integrity": "sha512-EN1Md2YvcC4sMqBoggsGYUEGlTNqUfJZWzduSt29fbQp1rKU2KlybTe+TWxKq/r2fFd+4JsRXxMeJiwB3w2AQA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/landmark": { + "version": "3.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.0-beta.18.tgz", + "integrity": "sha512-jFtWL7TYZrKucWNDx6ppUkGSqS2itkjhyLo9MIFqEg2mi4Lc2EoUjI/Gw9xMT+IJgebTcdQeXJpPskspl3Pojg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/link": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.8.tgz", + "integrity": "sha512-oiXUPQLZmf9Q9Xehb/sG1QRxfo28NFKdh9w+unD12sHI6NdLMETl5MA4CYyTgI0dfMtTjtfrF68GCnWfc7JvXQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/link": "^3.5.10", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/listbox": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.14.0.tgz", + "integrity": "sha512-pyVbKavh8N8iyiwOx6I3JIcICvAzFXkKSFni1yarfgngJsJV3KSyOkzLomOfN9UhbjcV4sX61/fccwJuvlurlA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/list": "^3.11.2", + "@react-types/listbox": "^3.5.4", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/live-announcer": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz", + "integrity": "sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.17.0.tgz", + "integrity": "sha512-aiFvSv3G1YvPC0klJQ/9quB05xIDZzJ5Lt6/CykP0UwGK5i8GCqm6/cyFLwEXsS5ooUPxS3bqmdOsgdADSSgqg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/overlays": "^3.25.0", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/collections": "^3.12.1", + "@react-stately/menu": "^3.9.1", + "@react-stately/selection": "^3.19.0", + "@react-stately/tree": "^3.8.7", + "@react-types/button": "^3.10.2", + "@react-types/menu": "^3.9.14", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/numberfield": { + "version": "3.11.10", + "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.11.10.tgz", + "integrity": "sha512-bYbTfO9NbAKMFOfEGGs+lvlxk0I9L0lU3WD2PFQZWdaoBz9TCkL+vK0fJk1zsuKaVjeGsmHP9VesBPRmaP0MiA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/spinbutton": "^3.6.11", + "@react-aria/textfield": "^3.16.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", + "@react-stately/numberfield": "^3.9.9", + "@react-types/button": "^3.10.2", + "@react-types/numberfield": "^3.8.8", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/overlays": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.25.0.tgz", + "integrity": "sha512-UEqJJ4duowrD1JvwXpPZreBuK79pbyNjNxFUVpFSskpGEJe3oCWwsSDKz7P1O7xbx5OYp+rDiY8fk/sE5rkaKw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/overlays": "^3.6.13", + "@react-types/button": "^3.10.2", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/progress": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.19.tgz", + "integrity": "sha512-5HHnBJHqEUuY+dYsjIZDYsENeKr49VCuxeaDZ0OSahbOlloIOB1baCo/6jLBv1O1rwrAzZ2gCCPcVGed/cjrcw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.5", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-types/progress": "^3.5.9", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/radio": { + "version": "3.10.11", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.10.11.tgz", + "integrity": "sha512-R150HsBFPr1jLMShI4aBM8heCa1k6h0KEvnFRfTAOBu+B9hMSZOPB+d6GQOwGPysNlbset90Kej8G15FGHjqiA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-stately/radio": "^3.10.10", + "@react-types/radio": "^3.8.6", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.26.0.tgz", + "integrity": "sha512-ZBH3EfWZ+RfhTj01dH8L17uT7iNbXWS8u77/fUpHgtrm0pwNVhx0TYVnLU1YpazQ/3WVpvWhmBB8sWwD1FlD/g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.2", + "@react-aria/i18n": "^3.12.13", + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-stately/selection": "^3.20.6", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection/node_modules/@internationalized/date": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", + "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/selection/node_modules/@react-aria/focus": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", + "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection/node_modules/@react-aria/i18n": { + "version": "3.12.13", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.13.tgz", + "integrity": "sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection/node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.7.15", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.15.tgz", + "integrity": "sha512-v9tujsuvJYRX0vE/vMYBzTT9FXbzrLsjkOrouNq+UdBIr7wRjIWTHHM0j+khb2swyCWNTbdv6Ce316Zqx2qWFg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-stately/slider": "^3.6.1", + "@react-types/shared": "^3.27.0", + "@react-types/slider": "^3.7.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton": { + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.19.tgz", + "integrity": "sha512-xOIXegDpts9t3RSHdIN0iYQpdts0FZ3LbpYJIYVvdEHo9OpDS+ElnDzCGtwZLguvZlwc5s1LAKuKopDUsAEMkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.13", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.31.0", + "@react-types/button": "^3.14.1", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@internationalized/date": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", + "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@react-aria/i18n": { + "version": "3.12.13", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.13.tgz", + "integrity": "sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.10.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@react-types/button": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.14.1.tgz", + "integrity": "sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/switch": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.11.tgz", + "integrity": "sha512-paYCpH+oeL+8rgQK+cBJ+IaZ1sXSh3+50WPlg2LvLBta0QVfQhPR4juPvfXRpfHHhCjFBgF4/RGbV8q5zpl3vA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/toggle": "^3.10.11", + "@react-stately/toggle": "^3.8.1", + "@react-types/shared": "^3.27.0", + "@react-types/switch": "^3.5.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/table": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.16.1.tgz", + "integrity": "sha512-T28TIGnKnPBunyErDBmm5jUX7AyzT7NVWBo9pDSt9wUuEnz0rVNd7p9sjmP2+u7I645feGG9klcdpCvFeqrk8A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/grid": "^3.11.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/live-announcer": "^3.4.1", + "@react-aria/utils": "^3.27.0", + "@react-aria/visually-hidden": "^3.8.19", + "@react-stately/collections": "^3.12.1", + "@react-stately/flags": "^3.0.5", + "@react-stately/table": "^3.13.1", + "@react-types/checkbox": "^3.9.1", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tabs": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.9.9.tgz", + "integrity": "sha512-oXPtANs16xu6MdMGLHjGV/2Zupvyp9CJEt7ORPLv5xAzSY5hSjuQHJLZ0te3Lh/KSG5/0o3RW/W5yEqo7pBQQQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/selection": "^3.22.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/tabs": "^3.7.1", + "@react-types/shared": "^3.27.0", + "@react-types/tabs": "^3.3.12", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/textfield": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.16.0.tgz", + "integrity": "sha512-53RVpMeMDN/QoabqnYZ1lxTh1xTQ3IBYQARuayq5EGGMafyxoFHzttxUdSqkZGK/+zdSF2GfmjOYJVm2nDKuDQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/form": "^3.0.12", + "@react-aria/label": "^3.7.14", + "@react-aria/utils": "^3.27.0", + "@react-stately/form": "^3.1.1", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@react-types/textfield": "^3.11.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toast": { + "version": "3.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.0-beta.19.tgz", + "integrity": "sha512-LCMTcmSmum5CzBk+DIec66q6pJGEl+InQPJdsby7QG/row0ka6wHPvul78HVseN7dzg6G3xVjvHtVPOixkuegA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.5", + "@react-aria/interactions": "^3.23.0", + "@react-aria/landmark": "3.0.0-beta.18", + "@react-aria/utils": "^3.27.0", + "@react-stately/toast": "3.0.0-beta.7", + "@react-types/button": "^3.10.2", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.12.2.tgz", + "integrity": "sha512-g25XLYqJuJpt0/YoYz2Rab8ax+hBfbssllcEFh0v0jiwfk2gwTWfRU9KAZUvxIqbV8Nm8EBmrYychDpDcvW1kw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-stately/toggle": "^3.9.2", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle/node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle/node_modules/@react-stately/toggle": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.9.2.tgz", + "integrity": "sha512-dOxs9wrVXHUmA7lc8l+N9NbTJMAaXcYsnNGsMwfXIXQ3rdq+IjWGNYJ52UmNQyRYFcg0jrzRrU16TyGbNjOdNQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.8", + "@react-types/checkbox": "^3.10.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle/node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle/node_modules/@react-types/checkbox": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.10.2.tgz", + "integrity": "sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.12", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.12.tgz", + "integrity": "sha512-a+Be27BtM2lzEdTzm19FikPbitfW65g/JZln3kyAvgpswhU6Ljl8lztaVw4ixjG4H0nqnKvVggMy4AlWwDUaVQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/i18n": "^3.12.5", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tooltip": { + "version": "3.7.11", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.11.tgz", + "integrity": "sha512-mhZgAWUj7bUWipDeJXaVPZdqnzoBCd/uaEbdafnvgETmov1udVqPTh9w4ZKX2Oh1wa2+OdLFrBOk+8vC6QbWag==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.19.1", + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-stately/tooltip": "^3.5.1", + "@react-types/shared": "^3.27.0", + "@react-types/tooltip": "^3.4.14", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", + "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils/node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden": { + "version": "3.8.28", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.28.tgz", + "integrity": "sha512-KRRjbVVob2CeBidF24dzufMxBveEUtUu7IM+hpdZKB+gxVROoh4XRLPv9SFmaH89Z7D9To3QoykVZoWD0lan6Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.6", + "@react-aria/utils": "^3.31.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden/node_modules/@react-aria/interactions": { + "version": "3.25.6", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", + "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.31.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/calendar": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.7.0.tgz", + "integrity": "sha512-N15zKubP2S7eWfPSJjKVlmJA7YpWzrIGx52BFhwLSQAZcV+OPcMgvOs71WtB7PLwl6DUYQGsgc0B3tcHzzvdvQ==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@react-stately/utils": "^3.10.5", + "@react-types/calendar": "^3.6.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/checkbox": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.6.11.tgz", + "integrity": "sha512-jApdBis+Q1sXLivg+f7krcVaP/AMMMiQcVqcz5gwxlweQN+dRZ/NpL0BYaDOuGc26Mp0lcuVaET3jIZeHwtyxA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.1.1", + "@react-stately/utils": "^3.10.5", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/collections": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.1.tgz", + "integrity": "sha512-8QmFBL7f+P64dEP4o35pYH61/lP0T/ziSdZAvNMrCqaM+fXcMfUp2yu1E63kADVX7WRDsFJWE3CVMeqirPH6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.10.2.tgz", + "integrity": "sha512-uT642Dool4tQBh+8UQjlJnTisrJVtg3LqmiP/HqLQ4O3pW0O+ImbG+2r6c9dUzlAnH4kEfmEwCp9dxkBkmFWsg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.1", + "@react-stately/form": "^3.1.1", + "@react-stately/list": "^3.11.2", + "@react-stately/overlays": "^3.6.13", + "@react-stately/select": "^3.6.10", + "@react-stately/utils": "^3.10.5", + "@react-types/combobox": "^3.13.2", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.12.0.tgz", + "integrity": "sha512-AfJEP36d+QgQ30GfacXtYdGsJvqY2yuCJ+JrjHct+m1nYuTkMvMMnhwNBFasgDJPLCDyHzyANlWkl2kQGfsBFw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@internationalized/string": "^3.2.5", + "@react-stately/form": "^3.1.1", + "@react-stately/overlays": "^3.6.13", + "@react-stately/utils": "^3.10.5", + "@react-types/datepicker": "^3.10.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/form": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.1.1.tgz", + "integrity": "sha512-qavrz5X5Mdf/Q1v/QJRxc0F8UTNEyRCNSM1we/nnF7GV64+aYSDLOtaRGmzq+09RSwo1c8ZYnIkK5CnwsPhTsQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.11.6.tgz", + "integrity": "sha512-vWPAkzpeTIsrurHfMubzMuqEw7vKzFhIJeEK5sEcLunyr1rlADwTzeWrHNbPMl66NAIAi70Dr1yNq+kahQyvMA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/selection": "^3.20.6", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid/node_modules/@react-stately/collections": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz", + "integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid/node_modules/@react-types/grid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.3.6.tgz", + "integrity": "sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/list": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.11.2.tgz", + "integrity": "sha512-eU2tY3aWj0SEeC7lH9AQoeAB4LL9mwS54FvTgHHoOgc1ZIwRJUaZoiuETyWQe98AL8KMgR1nrnDJ1I+CcT1Y7g==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.1", + "@react-stately/selection": "^3.19.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.1.tgz", + "integrity": "sha512-WRjGGImhQlQaer/hhahGytwd1BDq3fjpTkY/04wv3cQJPJR6lkVI5nSvGFMHfCaErsA1bNyB8/T9Y5F5u4u9ng==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.13", + "@react-types/menu": "^3.9.14", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/numberfield": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.9.9.tgz", + "integrity": "sha512-hZsLiGGHTHmffjFymbH1qVmA633rU2GNjMFQTuSsN4lqqaP8fgxngd5pPCoTCUFEkUgWjdHenw+ZFByw8lIE+g==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/number": "^3.6.0", + "@react-stately/form": "^3.1.1", + "@react-stately/utils": "^3.10.5", + "@react-types/numberfield": "^3.8.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.13", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.13.tgz", + "integrity": "sha512-WsU85Gf/b+HbWsnnYw7P/Ila3wD+C37Uk/WbU4/fHgJ26IEOWsPE6wlul8j54NZ1PnLNhV9Fn+Kffi+PaJMQXQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/overlays": "^3.8.12", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.10.10", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.10.10.tgz", + "integrity": "sha512-9x3bpq87uV8iYA4NaioTTWjriQSlSdp+Huqlxll0T3W3okpyraTTejE91PbIoRTUmL5qByIh2WzxYmr4QdBgAA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.1.1", + "@react-stately/utils": "^3.10.5", + "@react-types/radio": "^3.8.6", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.8.0.tgz", + "integrity": "sha512-A721nlt0DSCDit0wKvhcrXFTG5Vv1qkEVkeKvobmETZy6piKvwh0aaN8iQno5AFuZaj1iOZeNjZ/20TsDJR/4A==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.2", + "@react-stately/list": "^3.13.1", + "@react-stately/overlays": "^3.6.20", + "@react-stately/utils": "^3.10.8", + "@react-types/select": "^3.11.0", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-stately/collections": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz", + "integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-stately/form": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.2.2.tgz", + "integrity": "sha512-soAheOd7oaTO6eNs6LXnfn0tTqvOoe3zN9FvtIhhrErKz9XPc5sUmh3QWwR45+zKbitOi1HOjfA/gifKhZcfWw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-stately/list": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.13.1.tgz", + "integrity": "sha512-eHaoauh21twbcl0kkwULhVJ+CzYcy1jUjMikNVMHOQdhr4WIBdExf7PmSgKHKqsSPhpGg6IpTCY2dUX3RycjDg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/selection": "^3.20.6", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-stately/overlays": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.20.tgz", + "integrity": "sha512-YAIe+uI8GUXX8F/0Pzr53YeC5c/bjqbzDFlV8NKfdlCPa6+Jp4B/IlYVjIooBj9+94QvbQdjylegvYWK/iPwlg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.8", + "@react-types/overlays": "^3.9.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-types/overlays": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.2.tgz", + "integrity": "sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-types/select": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.11.0.tgz", + "integrity": "sha512-SzIsMFVPCbXE1Z1TLfpdfiwJ1xnIkcL1/CjGilmUKkNk5uT7rYX1xCJqWCjXI0vAU1xM4Qn+T3n8de4fw6HRBg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.20.6", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.20.6.tgz", + "integrity": "sha512-a0bjuP2pJYPKEiedz2Us1W1aSz0iHRuyeQEdBOyL6Z6VUa6hIMq9H60kvseir2T85cOa4QggizuRV7mcO6bU5w==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.8", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection/node_modules/@react-stately/collections": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz", + "integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection/node_modules/@react-stately/utils": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.6.1.tgz", + "integrity": "sha512-8kij5O82Xe233vZZ6qNGqPXidnlNQiSnyF1q613c7ktFmzAyGjkIWVUapHi23T1fqm7H2Rs3RWlmwE9bo2KecA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@react-types/slider": "^3.7.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/table": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.13.1.tgz", + "integrity": "sha512-Im8W+F8o9EhglY5kqRa3xcMGXl8zBi6W5phGpAjXb+UGDL1tBIlAcYj733bw8g/ITCnaSz9ubsmON0HekPd6Jg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.1", + "@react-stately/flags": "^3.0.5", + "@react-stately/grid": "^3.10.1", + "@react-stately/selection": "^3.19.0", + "@react-stately/utils": "^3.10.5", + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0", + "@react-types/table": "^3.10.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tabs": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.7.1.tgz", + "integrity": "sha512-gr9ACyuWrYuc727h7WaHdmNw8yxVlUyQlguziR94MdeRtFGQnf3V6fNQG3kxyB77Ljko69tgDF7Nf6kfPUPAQQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/list": "^3.11.2", + "@react-types/shared": "^3.27.0", + "@react-types/tabs": "^3.3.12", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toast": { + "version": "3.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.0.0-beta.7.tgz", + "integrity": "sha512-+KDkaOS5Y4ApOfiP0HHij4ySwAd1VM9/pI4rVTyHrzkp0R2Q0eBxZ74MpWMpVfJa2lSjb/qEm60tqJ3A+4R/cQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toggle": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.8.1.tgz", + "integrity": "sha512-MVpe79ghVQiwLmVzIPhF/O/UJAUc9B+ZSylVTyJiEPi0cwhbkKGQv9thOF0ebkkRkace5lojASqUAYtSTZHQJA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.5", + "@react-types/checkbox": "^3.9.1", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tooltip": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.1.tgz", + "integrity": "sha512-0aI3U5kB7Cop9OCW9/Bag04zkivFSdUcQgy/TWL4JtpXidVWmOha8txI1WySawFSjZhH83KIyPc+wKm1msfLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.13", + "@react-types/tooltip": "^3.4.14", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tree": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.8.7.tgz", + "integrity": "sha512-hpc3pyuXWeQV5ufQ02AeNQg/MYhnzZ4NOznlY5OOUoPzpLYiI3ZJubiY3Dot4jw5N/LR7CqvDLHmrHaJPmZlHg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.1", + "@react-stately/selection": "^3.19.0", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", + "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/virtualizer": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-4.2.1.tgz", + "integrity": "sha512-GHGEXV0ZRhq34U/P3LzkByCBfy2IDynYlV1SE4njkUWWGE/0AH56UegM6w2l3GeiNpXsXCgXl7jpAKeIGMEnrQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/accordion": { + "version": "3.0.0-alpha.26", + "resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.26.tgz", + "integrity": "sha512-OXf/kXcD2vFlEnkcZy/GG+a/1xO9BN7Uh3/5/Ceuj9z2E/WwD55YwU3GFM5zzkZ4+DMkdowHnZX37XnmbyD3Mg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.10", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.10.tgz", + "integrity": "sha512-5HhRxkKHfAQBoyOYzyf4HT+24HgPE/C/QerxJLNNId303LXO03yeYrbvRqhYZSlD1ACLJW9OmpPpREcw5iSqgw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/link": "^3.5.10", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/button": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.10.2.tgz", + "integrity": "sha512-h8SB/BLoCgoBulCpyzaoZ+miKXrolK9XC48+n1dKJXT8g4gImrficurDW6+PRTQWaRai0Q0A6bu8UibZOU4syg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/calendar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.6.0.tgz", + "integrity": "sha512-BtFh4BFwvsYlsaSqUOVxlqXZSlJ6u4aozgO3PwHykhpemwidlzNwm9qDZhcMWPioNF/w2cU/6EqhvEKUHDnFZg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/checkbox": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.9.1.tgz", + "integrity": "sha512-0x/KQcipfNM9Nvy6UMwYG25roRLvsiqf0J3woTYylNNWzF+72XT0iI5FdJkE3w2wfa0obmSoeq4WcbFREQrH/A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/combobox": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.2.tgz", + "integrity": "sha512-yl2yMcM5/v3lJiNZWjpAhQ9vRW6dD55CD4rYmO2K7XvzYJaFVT4WYI/AymPYD8RqomMp7coBmBHfHW0oupk8gg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/datepicker": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.10.0.tgz", + "integrity": "sha512-Att7y4NedNH1CogMDIX9URXgMLxGbZgnFCZ8oxgFAVndWzbh3TBcc4s7uoJDPvgRMAalq+z+SrlFFeoBeJmvvg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.7.0", + "@react-types/calendar": "^3.6.0", + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/dialog": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.22.tgz", + "integrity": "sha512-smSvzOcqKE196rWk0oqJDnz+ox5JM5+OT0PmmJXiUD4q7P5g32O6W5Bg7hMIFUI9clBtngo8kLaX2iMg+GqAzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.2", + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/dialog/node_modules/@react-types/overlays": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.2.tgz", + "integrity": "sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/dialog/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/form": { + "version": "3.7.9", + "resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.9.tgz", + "integrity": "sha512-+qGDrQFdIh8umU82zmnYJ0V2rLoGSQ3yApFT02URz//NWeTA7qo0Oab2veKvXUkcBb47oSvytZYmkExPikxIEg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/grid": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.11.tgz", + "integrity": "sha512-Mww9nrasppvPbsBi+uUqFnf7ya8fXN0cTVzDNG+SveD8mhW+sbtuy+gPtEpnFD2Oyi8qLuObefzt4gdekJX2Yw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/link": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.10.tgz", + "integrity": "sha512-IM2mbSpB0qP44Jh1Iqpevo7bQdZAr0iDyDi13OhsiUYJeWgPMHzGEnQqdBMkrfQeOTXLtZtUyOYLXE2v39bhzQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/listbox": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.7.4.tgz", + "integrity": "sha512-p4YEpTl/VQGrqVE8GIfqTS5LkT5jtjDTbVeZgrkPnX/fiPhsfbTPiZ6g0FNap4+aOGJFGEEZUv2q4vx+rCORww==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/listbox/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/menu": { + "version": "3.9.14", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.14.tgz", + "integrity": "sha512-RJW/S8IPwbRuohJ/A9HJ7W8QaAY816tm7Nv6+H/TLXG76zu2AS5vEgq+0TcCAWvJJwUdLDpJWJMlo0iIoIBtcg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/numberfield": { + "version": "3.8.8", + "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.8.tgz", + "integrity": "sha512-825JPppxDaWh0Zxb0Q+wSslgRQYOtQPCAuhszPuWEy6d2F/M+hLR+qQqvQm9+LfMbdwiTg6QK5wxdWFCp2t7jw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/overlays": { + "version": "3.8.12", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.12.tgz", + "integrity": "sha512-ZvR1t0YV7/6j+6OD8VozKYjvsXT92+C/2LOIKozy7YUNS5KI4MkXbRZzJvkuRECVZOmx8JXKTUzhghWJM/3QuQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/progress": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.9.tgz", + "integrity": "sha512-zFxOzx3G8XUmHgpm037Hcayls5bqzXVa182E3iM7YWTmrjxJPKZ58XL0WWBgpTd+mJD7fTpnFdAZqSmFbtDOdA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/radio": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.8.6.tgz", + "integrity": "sha512-woTQYdRFjPzuml4qcIf+2zmycRuM5w3fDS5vk6CQmComVUjOFPtD28zX3Z9kc9lSNzaBQz9ONZfFqkZ1gqfICA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/select": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.9.tgz", + "integrity": "sha512-/hCd0o+ztn29FKCmVec+v7t4JpOzz56o+KrG7NDq2pcRWqUR9kNwCjrPhSbJIIEDm4ubtrfPu41ysIuDvRd2Bg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz", + "integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/slider": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.8.2.tgz", + "integrity": "sha512-MQYZP76OEOYe7/yA2To+Dl0LNb0cKKnvh5JtvNvDnAvEprn1RuLiay8Oi/rTtXmc2KmBa4VdTcsXsmkbbkeN2Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/slider/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/switch": { + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.15.tgz", + "integrity": "sha512-r/ouGWQmIeHyYSP1e5luET+oiR7N7cLrAlWsrAfYRWHxqXOSNQloQnZJ3PLHrKFT02fsrQhx2rHaK2LfKeyN3A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.32.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/switch/node_modules/@react-types/shared": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", + "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/table": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.10.4.tgz", + "integrity": "sha512-d0tLz/whxVteqr1rophtuuxqyknHHfTKeXrCgDjt8pAyd9U8GPDbfcFSfYPUhWdELRt7aLVyQw6VblZHioVEgQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/grid": "^3.2.11", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tabs": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.12.tgz", + "integrity": "sha512-E9O9G+wf9kaQ8UbDEDliW/oxYlJnh7oDCW1zaMOySwnG4yeCh7Wu02EOCvlQW4xvgn/i+lbEWgirf7L+yj5nRg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/textfield": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.11.0.tgz", + "integrity": "sha512-YORBgr6wlu2xfvr4MqjKFHGpj+z8LBzk14FbWDbYnnhGnv0I10pj+m2KeOHgDNFHrfkDdDOQmMIKn1UCqeUuEg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tooltip": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.14.tgz", + "integrity": "sha512-J7CeYL2yPeKIasx1rPaEefyCHGEx2DOCx+7bM3XcKGmCxvNdVQLjimNJOt8IHlUA0nFJQOjmSW/mz9P0f2/kUw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.8.12", + "@react-types/shared": "^3.27.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz", + "integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.91.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz", + "integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz", + "integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.1.tgz", + "integrity": "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.91.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.10", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz", + "integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.11.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz", + "integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "license": "MIT" + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color2k": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.263", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", + "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", + "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/input-otp": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", + "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.0.tgz", + "integrity": "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.0.tgz", + "integrity": "sha512-Q4haR150pN/5N75O30iIsRJcr3ef7p7opFaKpcaREy0GQit6uCRu1NEiIFIwnHJQy0bsziRFBweR/5EkmHgVUQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.10.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz", + "integrity": "sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.2.1.tgz", + "integrity": "sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tailwind-merge": "^2.2.0" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, + "node_modules/tailwind-variants/node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..7b5e9b0c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "playwright-reports-server-frontend", + "version": "5.7.4", + "description": "Playwright Reports Server Frontend", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@heroui/link": "2.2.12", + "@heroui/navbar": "2.2.13", + "@heroui/react": "2.7.4", + "@heroui/switch": "2.2.13", + "@heroui/system": "2.4.11", + "@heroui/theme": "2.4.11", + "@react-aria/selection": "^3.12.1", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.15.0", + "@react-aria/visually-hidden": "^3.8.28", + "@tanstack/react-query": "^5.59.15", + "@tanstack/react-query-devtools": "^5.59.15", + "clsx": "^2.1.1", + "framer-motion": "^11.11.8", + "next-themes": "^0.4.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.0.2", + "recharts": "^2.13.0", + "sonner": "^1.5.0", + "tailwind-merge": "^3.0.2" + }, + "devDependencies": { + "@types/react": "18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "10.4.20", + "postcss": "8.4.47", + "tailwind-variants": "0.2.1", + "tailwindcss": "3.4.13", + "typescript": "5.2.2", + "vite": "^6.0.3" + } +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..0c606b50 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,32 @@ +import { Route, Routes } from "react-router-dom"; +import { Toaster } from "sonner"; +import { Layout } from "./components/Layout"; +import HomePage from "./pages/HomePage"; +import LoginPage from "./pages/LoginPage"; +import ReportDetailPage from "./pages/ReportDetailPage"; +import ReportsPage from "./pages/ReportsPage"; +import ResultsPage from "./pages/ResultsPage"; +import SettingsPage from "./pages/SettingsPage"; +import TrendsPage from "./pages/TrendsPage"; +import { Providers } from "./providers"; + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} + +export default App; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 00000000..46112dbd --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from "react"; +import { Aside } from "./aside"; +import { Navbar } from "./navbar"; + +interface LayoutProps { + children: ReactNode; +} + +export function Layout({ children }: LayoutProps) { + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/aside.tsx b/frontend/src/components/aside.tsx new file mode 100644 index 00000000..0fac9e7b --- /dev/null +++ b/frontend/src/components/aside.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Badge, Card, CardBody, Link } from "@heroui/react"; +import { Link as RouterLink, useLocation } from "react-router-dom"; +import { siteConfig } from "../config/site"; +import { useAuth } from "../hooks/useAuth"; +import { useAuthConfig } from "../hooks/useAuthConfig"; +import useQuery from "../hooks/useQuery"; +import { ReportIcon, ResultIcon, SettingsIcon, TrendIcon } from "./icons"; + +interface ServerInfo { + numOfReports: number; + numOfResults: number; +} + +const iconst = [ + { href: "/reports", icon: ReportIcon }, + { href: "/results", icon: ResultIcon }, + { href: "/trends", icon: TrendIcon }, + { href: "/settings", icon: SettingsIcon }, +]; + +export const Aside: React.FC = () => { + const location = useLocation(); + const pathname = location.pathname; + const session = useAuth(); + const { authRequired } = useAuthConfig(); + const isAuthenticated = + authRequired === false || session.status === "authenticated"; + + const { data: serverInfo } = useQuery("/api/info", { + enabled: isAuthenticated, + }); + + return ( + + +
+ {siteConfig.navItems.map((item) => { + const isActive = pathname === item.href; + const Icon = iconst.find((icon) => icon.href === item.href)?.icon; + const count = + item.href === "/reports" + ? serverInfo?.numOfReports + : item.href === "/results" + ? serverInfo?.numOfResults + : 0; + + return ( + + {count !== undefined && count > 0 ? ( + + {Icon && } + + ) : ( + Icon && + )} + + ); + })} +
+
+
+ ); +}; diff --git a/frontend/src/components/date-format.tsx b/frontend/src/components/date-format.tsx new file mode 100644 index 00000000..2a8c6665 --- /dev/null +++ b/frontend/src/components/date-format.tsx @@ -0,0 +1,16 @@ +"use client"; +import { useEffect, useState } from "react"; + +/** + * Specific method for date formatting on the client + * as server locale and client locale may not match + */ +export default function FormattedDate({ date }: Readonly<{ date: Date }>) { + const [formattedDate, setFormattedDate] = useState(""); + + useEffect(() => { + setFormattedDate(new Date(date).toLocaleString()); + }, [date]); + + return {formattedDate}; +} diff --git a/frontend/src/components/delete-report-button.tsx b/frontend/src/components/delete-report-button.tsx new file mode 100644 index 00000000..1a123b95 --- /dev/null +++ b/frontend/src/components/delete-report-button.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + useDisclosure, +} from "@heroui/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import useMutation from "../hooks/useMutation"; +import { invalidateCache } from "../lib/query-cache"; +import { DeleteIcon } from "./icons"; + +interface DeleteProjectButtonProps { + reportId: string; + onDeleted: () => void; +} + +export default function DeleteReportButton({ + reportId, + onDeleted, +}: DeleteProjectButtonProps) { + const queryClient = useQueryClient(); + const { + mutate: deleteReport, + isPending, + error, + } = useMutation("/api/report/delete", { + method: "DELETE", + onSuccess: () => { + invalidateCache(queryClient, { + queryKeys: ["/api/info"], + predicate: "/api/report", + }); + toast.success(`report "${reportId}" deleted`); + }, + }); + + const { isOpen, onOpen, onOpenChange } = useDisclosure(); + + const DeleteReport = async () => { + if (!reportId) { + return; + } + + deleteReport({ body: { reportsIds: [reportId] } }); + + onDeleted?.(); + }; + + error && toast.error(error.message); + + return ( + !!reportId && ( + <> + + + + {(onClose) => ( + <> + + Are you sure? + + +

This will permanently delete your report

+
+ + + + + + )} +
+
+ + ) + ); +} diff --git a/frontend/src/components/delete-results-button.tsx b/frontend/src/components/delete-results-button.tsx new file mode 100644 index 00000000..003d3ae4 --- /dev/null +++ b/frontend/src/components/delete-results-button.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + useDisclosure, +} from "@heroui/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import useMutation from "../hooks/useMutation"; +import { invalidateCache } from "../lib/query-cache"; +import { DeleteIcon } from "./icons"; + +interface DeleteProjectButtonProps { + resultIds: string[]; + onDeletedResult?: () => void; + label?: string; +} + +export default function DeleteResultsButton({ + resultIds, + onDeletedResult, + label, +}: Readonly) { + const queryClient = useQueryClient(); + const { + mutate: deleteResult, + isPending, + error, + } = useMutation("/api/result/delete", { + method: "DELETE", + onSuccess: () => { + invalidateCache(queryClient, { + queryKeys: ["/api/info"], + predicate: "/api/result", + }); + toast.success( + `result${resultIds.length ? "" : "s"} ${resultIds ?? "are"} deleted`, + ); + }, + }); + + const { isOpen, onOpen, onOpenChange } = useDisclosure(); + + const DeleteResult = async () => { + if (!resultIds?.length) { + return; + } + + deleteResult({ body: { resultsIds: resultIds } }); + + onDeletedResult?.(); + }; + + error && toast.error(error.message); + + return ( + <> + + + + {(onClose) => ( + <> + + Are you sure? + + +

This will permanently delete your results files.

+
+ + + + + + )} +
+
+ + ); +} diff --git a/frontend/src/components/generate-report-button.tsx b/frontend/src/components/generate-report-button.tsx new file mode 100644 index 00000000..3a93fcef --- /dev/null +++ b/frontend/src/components/generate-report-button.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { + Autocomplete, + AutocompleteItem, + Button, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + useDisclosure, +} from "@heroui/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import type { Result } from "@/types/storage"; +import useMutation from "../hooks/useMutation"; +import useQuery from "../hooks/useQuery"; +import { invalidateCache } from "../lib/query-cache"; + +interface DeleteProjectButtonProps { + results: Result[]; + projects: string[]; + onGeneratedReport?: () => void; +} + +export default function GenerateReportButton({ + results, + projects, + onGeneratedReport, +}: Readonly) { + const queryClient = useQueryClient(); + const [generationError, setGenerationError] = useState(null); + + const { mutate: generateReport, isPending } = useMutation( + "/api/report/generate", + { + method: "POST", + onSuccess: (data: { reportId: string }) => { + invalidateCache(queryClient, { + queryKeys: ["/api/info"], + predicate: "/api/report", + }); + invalidateCache(queryClient, { + queryKeys: ["/api/info"], + predicate: "/api/result", + }); + toast.success(`report ${data?.reportId} is generated`); + setProjectName(""); + setCustomName(""); + setGenerationError(null); + onClose(); + onGeneratedReport?.(); + }, + onError: (err: Error) => { + let errorMessage = err.message; + if ( + err.message.includes("ENOENT") || + err.message.includes("not found") || + err.message.includes("404") + ) { + errorMessage = + "One or more selected results were not found. Please refresh the page and try again."; + } else if (err.message.includes("ResultID not found")) { + errorMessage = + "Some selected results no longer exist. Please refresh the page and select valid results."; + } + + setGenerationError(errorMessage); + }, + }, + ); + + const { + data: resultProjects, + error: resultProjectsError, + isLoading: isResultProjectsLoading, + } = useQuery(`/api/result/projects`); + + const [projectName, setProjectName] = useState(""); + const [customName, setCustomName] = useState(""); + + useEffect(() => { + !projectName && setProjectName(projects?.at(0) ?? ""); + }, [projects, projectName]); + + const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); + + const handleModalOpen = () => { + setGenerationError(null); + onOpen(); + }; + + const GenerateReport = async () => { + if (!results?.length) { + return; + } + + setGenerationError(null); + + const validResults = results.filter( + (r) => r.resultID && r.resultID.trim() !== "", + ); + if (validResults.length !== results.length) { + setGenerationError("Some selected results are invalid or missing IDs"); + return; + } + + const resultIds = validResults.map((r) => r.resultID); + generateReport({ + body: { resultsIds: resultIds, project: projectName, title: customName }, + }); + }; + + return ( + <> + + + + {(onClose) => ( + <> + + Generate report + + + {generationError ? ( +
+

+ Report generation failed: +

+
+											{generationError}
+										
+
+ ) : ( + <> + ({ + label: project, + value: project, + }))} + label="Project name" + labelPlacement="outside" + placeholder="leave empty if not required" + variant="bordered" + onInputChange={(value) => setProjectName(value)} + onSelectionChange={(value) => + value && setProjectName(value?.toString() ?? "") + } + > + {(item) => ( + + {item.label} + + )} + + ) => + setCustomName(e.target.value ?? "") + } + onClear={() => setCustomName("")} + /> + + )} +
+ + + {!generationError && ( + + )} + + + )} +
+
+ + ); +} diff --git a/frontend/src/components/header-links.tsx b/frontend/src/components/header-links.tsx new file mode 100644 index 00000000..f934a02b --- /dev/null +++ b/frontend/src/components/header-links.tsx @@ -0,0 +1,50 @@ +import { Link } from "@heroui/link"; +import type { SiteWhiteLabelConfig } from "../types"; +import { + BitbucketIcon, + CyborgTestIcon, + DiscordIcon, + GithubIcon, + LinkIcon, + SlackIcon, + TelegramIcon, +} from "./icons"; + +interface HeaderLinksProps { + config: SiteWhiteLabelConfig; + withTitle?: boolean; +} + +export const HeaderLinks: React.FC = ({ + config, + withTitle = false, +}) => { + const links = config?.headerLinks; + + const availableSocialLinkIcons = [ + { name: "telegram", Icon: TelegramIcon, title: "Telegram" }, + { name: "discord", Icon: DiscordIcon, title: "Discord" }, + { name: "github", Icon: GithubIcon, title: "GitHub" }, + { name: "cyborgTest", Icon: CyborgTestIcon, title: "Cyborg Test" }, + { name: "bitbucket", Icon: BitbucketIcon, title: "Bitbucket" }, + { name: "slack", Icon: SlackIcon, title: "Slack" }, + ]; + + const socialLinks = Object.entries(links).map(([name, href]) => { + const availableLink = availableSocialLinkIcons.find( + (available) => available.name === name, + ); + + const Icon = availableLink?.Icon ?? LinkIcon; + const title = availableLink?.title ?? name; + + return href ? ( + + + {withTitle &&

{title}

} + + ) : null; + }); + + return socialLinks; +}; diff --git a/frontend/src/components/icons.tsx b/frontend/src/components/icons.tsx new file mode 100644 index 00000000..644d1949 --- /dev/null +++ b/frontend/src/components/icons.tsx @@ -0,0 +1,436 @@ +import type { FC } from "react"; + +import type { IconSvgProps } from "../types"; + +export const DiscordIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const GithubIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const SlackIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => ( + + + + + + +); + +export const BitbucketIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + + + + + ); +}; + +export const CyborgTestIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + + + ); +}; + +export const TelegramIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const MoonFilledIcon = ({ + size = 40, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SunFilledIcon = ({ + size = 40, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const LinkIcon: FC = ({ width, height, ...props }) => { + return ( + + + + + ); +}; + +export const ReportIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + + + ); +}; + +export const ResultIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + + + ); +}; + +export const TrendIcon: FC = ({ + size = 40, + width, + height, + ...props +}) => { + return ( + + + + + + ); +}; + +export const DeleteIcon: FC = (props) => ( + +); + +export const SearchIcon: FC = (props) => ( + +); + +export const BranchIcon: FC = ({ width, height, ...props }) => { + return ( + + + + ); +}; + +export const FolderIcon: FC = ({ width, height, ...props }) => { + return ( + + + + ); +}; + +export const SettingsIcon: FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + + ); +}; diff --git a/frontend/src/components/inline-stats-circle.tsx b/frontend/src/components/inline-stats-circle.tsx new file mode 100644 index 00000000..b776cf73 --- /dev/null +++ b/frontend/src/components/inline-stats-circle.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { CircularProgress } from "@heroui/react"; +import type { FC } from "react"; + +import type { ReportStats } from "../types/parser"; + +type ReportFiltersProps = { + stats: ReportStats; +}; + +const InlineStatsCircle: FC = ({ stats }) => { + if (!stats.total) return null; + + const passedPercentage = + ((stats.expected || 0) / (stats.total - (stats.skipped || 0))) * 100; + + return ( + + ); +}; + +export default InlineStatsCircle; diff --git a/frontend/src/components/jira-ticket-modal.tsx b/frontend/src/components/jira-ticket-modal.tsx new file mode 100644 index 00000000..1486fad5 --- /dev/null +++ b/frontend/src/components/jira-ticket-modal.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { + Button, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Select, + SelectItem, + Textarea, +} from "@heroui/react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +import type { ReportTest } from "../types/parser"; +import type { JiraApiResponse } from "../types/index"; + +interface JiraTicketModalProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + test: ReportTest | null; + reportId?: string; +} + +interface JiraTicketData { + summary: string; + description: string; + issueType: string; + projectKey: string; +} + +export default function JiraTicketModal({ + isOpen, + onOpenChange, + test, + reportId, +}: JiraTicketModalProps) { + const [ticketData, setTicketData] = useState({ + summary: "", + description: "", + issueType: "Bug", + projectKey: "", + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [jiraConfig, setJiraConfig] = useState(null); + const [isLoadingConfig, setIsLoadingConfig] = useState(true); + + useEffect(() => { + const loadJiraConfig = async () => { + try { + const response = await fetch("/api/jira/config"); + const config = await response.json(); + + setJiraConfig(config); + + if ( + config.configured && + config.defaultProjectKey && + !ticketData.projectKey + ) { + setTicketData((prev) => ({ + ...prev, + projectKey: config.defaultProjectKey, + })); + } + + if (config.configured) { + const newDefaults: Partial = {}; + + if (config.issueTypes?.length > 0 && ticketData.issueType === "Bug") { + newDefaults.issueType = config.issueTypes[0].name; + } + + if (Object.keys(newDefaults).length > 0) { + setTicketData((prev) => ({ ...prev, ...newDefaults })); + } + } + } catch (error) { + console.error("Failed to load Jira configuration:", error); + } finally { + setIsLoadingConfig(false); + } + }; + + if (isOpen) { + loadJiraConfig(); + } + }, [isOpen]); + + const handleSubmit = async () => { + if (!test) return; + + setIsSubmitting(true); + + try { + const testAttachments = + test.attachments?.map((att) => ({ + name: att.name, + path: att.path, + contentType: att.contentType, + })) || []; + + const requestData = { + ...ticketData, + testId: test.testId, + testTitle: test.title, + testOutcome: test.outcome, + testLocation: test.location, + testAttachments, + reportId: reportId, + }; + + const response = await fetch("/api/jira/create-ticket", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestData), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "Failed to create Jira ticket"); + } + + toast.success(`Jira ticket created: ${result.issueKey}`); + onOpenChange(false); + + setTicketData({ + summary: "", + description: "", + issueType: "Bug", + projectKey: ticketData.projectKey, // Keep the current project key + }); + } catch (error) { + toast.error( + `Failed to create Jira ticket: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } finally { + setIsSubmitting(false); + } + }; + + const generateDefaultSummary = () => { + if (!test) return ""; + + return `Test Failed: ${test.title}`; + }; + + const generateDefaultDescription = () => { + if (!test) return ""; + + return `Test Failure Details + Test: ${test.title} + Project: ${test.projectName || "Unknown"} + Location: ${test.location?.file || "Unknown"}:${test.location?.line || "Unknown"} + Test ID: ${test.testId || "Unknown"} + + Steps to Reproduce: + 1. Run the test suite + 2. Test "${test.title}" fails + + Expected Behavior: + Test should pass + + Actual Behavior: + Test is failing + + Additional Information: + - Duration: ${test.duration || 0}ms + - Tags: ${test.tags?.join(", ") || "None"} + - Annotations: ${test.annotations?.map((a) => a.description).join(", ") || "None"}`; + }; + + // Auto-populate form when test changes + if (test && (!ticketData.summary || ticketData.summary === "")) { + setTicketData((prev) => ({ + ...prev, + summary: generateDefaultSummary(), + description: generateDefaultDescription(), + })); + } + + return ( + + + {(onClose) => ( + <> + + Create Jira Ticket + + + {isLoadingConfig ? ( +
+
+
+

+ Loading Jira configuration... +

+
+
+ ) : !jiraConfig?.configured ? ( +
+
+ + + +
+

+ Jira Not Configured +

+

+ {jiraConfig?.message || + "Jira integration is not properly configured."} +

+
+

+ Required Environment Variables: +

+
    +
  • โ€ข JIRA_BASE_URL
  • +
  • โ€ข JIRA_EMAIL
  • +
  • โ€ข JIRA_API_TOKEN
  • +
+
+
+ ) : ( +
+ + setTicketData((prev) => ({ ...prev, summary: value })) + } + /> +