From a8374e7d6c0edd1a3898682201e217fffe6ced2f Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 19 Aug 2026 14:37:49 +0330 Subject: [PATCH] Add digital catalog with shareable flipbook and ktg.montzar.ir deploy files. Co-authored-by: Cursor --- .env.example | 12 + .gitignore | 8 + README.md | 53 + client/index.html | 18 + client/src/App.vue | 7 + client/src/api.ts | 49 + client/src/components/CatalogViewer.vue | 217 + client/src/components/EffectSelect.vue | 28 + client/src/components/PageList.vue | 28 + .../src/components/effects/CssPageEffect.vue | 202 + .../src/components/effects/MagazineEffect.vue | 253 + .../src/components/effects/ScrollEffect.vue | 73 + client/src/constants.ts | 9 + client/src/env.d.ts | 21 + client/src/images.ts | 64 + client/src/main.ts | 6 + client/src/pages/CreatePage.vue | 233 + client/src/pages/DeletePage.vue | 56 + client/src/pages/DonePage.vue | 74 + client/src/pages/ViewPage.vue | 47 + client/src/pdf.ts | 74 + client/src/router.ts | 15 + client/src/styles.css | 451 ++ deploy/.env.production.example | 6 + deploy/DEPLOY.md | 167 + deploy/ecosystem.config.cjs | 26 + deploy/nginx-ktg.montzar.ir.conf | 28 + docs/PROJECT.md | 342 ++ .../plans/2026-08-18-digital-catalog.md | 366 ++ package-lock.json | 4369 +++++++++++++++++ package.json | 38 + server/app.ts | 44 + server/catalog-repo.ts | 79 + server/db.ts | 58 + server/env.ts | 30 + server/errors.ts | 19 + server/index.ts | 30 + server/magic.ts | 19 + server/qr.ts | 10 + server/rate-limit.ts | 25 + server/routes/catalogs.ts | 198 + server/schema.ts | 11 + server/storage.ts | 75 + shared/types.ts | 53 + tests/server/catalog-repo.test.ts | 49 + tests/server/catalogs.test.ts | 225 + tests/server/helpers.ts | 10 + tests/server/rate-limit.test.ts | 44 + tests/server/storage.test.ts | 48 + tsconfig.app.json | 24 + tsconfig.json | 7 + tsconfig.node.json | 25 + vite.config.ts | 29 + vitest.config.ts | 10 + 54 files changed, 8462 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 client/index.html create mode 100644 client/src/App.vue create mode 100644 client/src/api.ts create mode 100644 client/src/components/CatalogViewer.vue create mode 100644 client/src/components/EffectSelect.vue create mode 100644 client/src/components/PageList.vue create mode 100644 client/src/components/effects/CssPageEffect.vue create mode 100644 client/src/components/effects/MagazineEffect.vue create mode 100644 client/src/components/effects/ScrollEffect.vue create mode 100644 client/src/constants.ts create mode 100644 client/src/env.d.ts create mode 100644 client/src/images.ts create mode 100644 client/src/main.ts create mode 100644 client/src/pages/CreatePage.vue create mode 100644 client/src/pages/DeletePage.vue create mode 100644 client/src/pages/DonePage.vue create mode 100644 client/src/pages/ViewPage.vue create mode 100644 client/src/pdf.ts create mode 100644 client/src/router.ts create mode 100644 client/src/styles.css create mode 100644 deploy/.env.production.example create mode 100644 deploy/DEPLOY.md create mode 100644 deploy/ecosystem.config.cjs create mode 100644 deploy/nginx-ktg.montzar.ir.conf create mode 100644 docs/PROJECT.md create mode 100644 docs/superpowers/plans/2026-08-18-digital-catalog.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/app.ts create mode 100644 server/catalog-repo.ts create mode 100644 server/db.ts create mode 100644 server/env.ts create mode 100644 server/errors.ts create mode 100644 server/index.ts create mode 100644 server/magic.ts create mode 100644 server/qr.ts create mode 100644 server/rate-limit.ts create mode 100644 server/routes/catalogs.ts create mode 100644 server/schema.ts create mode 100644 server/storage.ts create mode 100644 shared/types.ts create mode 100644 tests/server/catalog-repo.test.ts create mode 100644 tests/server/catalogs.test.ts create mode 100644 tests/server/helpers.ts create mode 100644 tests/server/rate-limit.test.ts create mode 100644 tests/server/storage.test.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4703a7a --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +PORT=3000 +API_PORT=3010 +PUBLIC_BASE_URL=http://localhost:3000 +STORAGE_DIR=./storage +DATABASE_PATH=./data/catalogs.sqlite + +# Production (server /var/www/ktg) — see deploy/DEPLOY.md +# NODE_ENV=production +# PORT=3020 +# PUBLIC_BASE_URL=https://ktg.montzar.ir +# STORAGE_DIR=/var/www/ktg/storage +# DATABASE_PATH=/var/www/ktg/data/catalogs.sqlite diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10dd953 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +storage +data +.env +*.log +.DS_Store +catalog-deploy.tar diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f8690e --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# کاتالوگ دیجیتال + +از PDF یا چند تصویر، یک کاتالوگ ورق‌زدنی با لینک و QR می‌سازد. ثبت‌نام ندارد. کاتالوگ‌ها در سایت لیست نمی‌شوند. + +جزئیات محصول: [`docs/PROJECT.md`](docs/PROJECT.md) + +## اجرا + +نیاز به Node.js ۲۰ یا جدیدتر. + +```bash +copy .env.example .env +npm install +npm run dev +``` + +سپس مرورگر را روی [http://localhost:3000](http://localhost:3000) باز کنید. + +- فرانت روی پورت ۳۰۰۰ — فقط همین را در مرورگر باز کنید +- API روی پورت ۳۰۱۰ (از طریق `/api` پروکسی می‌شود) + +اگر پورت ۳۰۰۰ اشغال باشد Vite جابه‌جا نمی‌شود. همه ترمینال‌های `npm run dev` را ببندید و یکی را دوباره اجرا کنید. + +## ساخت برای سرور + +```bash +npm run build +npm start +``` + +`npm start` هم فرانت ساخته‌شده و هم API را روی `PORT` (پیش‌فرض ۳۰۰۰) سرو می‌کند. + +استقرار روی سرور مشترک (`ktg.montzar.ir`): راهنمای قدم‌به‌قدم [`deploy/DEPLOY.md`](deploy/DEPLOY.md). + +## تست + +```bash +npm test +``` + +## متغیرهای محیط + +در `.env`: + +``` +PORT=3000 +API_PORT=3001 +PUBLIC_BASE_URL=http://localhost:3000 +STORAGE_DIR=./storage +DATABASE_PATH=./data/catalogs.sqlite +``` + +`PUBLIC_BASE_URL` همان آدرسی است که داخل QR نوشته می‌شود. روی دامنه واقعی آن را به دامنه خودتان عوض کنید. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..a4b27bb --- /dev/null +++ b/client/index.html @@ -0,0 +1,18 @@ + + + + + + کاتالوگ دیجیتال + + + + + +
+ + + diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 0000000..02dd6ec --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/client/src/api.ts b/client/src/api.ts new file mode 100644 index 0000000..6d1bfd0 --- /dev/null +++ b/client/src/api.ts @@ -0,0 +1,49 @@ +import type { CatalogPublic, CreateCatalogResponse, EffectId } from "@shared/types"; + +async function readError(res: Response): Promise { + try { + const body = (await res.json()) as { error?: string }; + return body.error || "خطا در ارتباط با سرور"; + } catch { + return "خطا در ارتباط با سرور"; + } +} + +export async function createCatalog(input: { + title: string; + effect: EffectId; + pages: Blob[]; + flipMs: number; +}): Promise { + const data = new FormData(); + data.set("title", input.title); + data.set("effect", input.effect); + data.set("flipMs", String(input.flipMs)); + input.pages.forEach((page, i) => { + data.append("pages", page, `page-${String(i + 1).padStart(3, "0")}.jpg`); + }); + const res = await fetch("/api/catalogs", { method: "POST", body: data }); + if (!res.ok) throw new Error(await readError(res)); + return res.json(); +} + +export async function getCatalog(id: string): Promise { + const res = await fetch(`/api/catalogs/${id}`); + if (!res.ok) throw new Error(await readError(res)); + return res.json(); +} + +export function pageUrl(id: string, n: number): string { + return `/api/catalogs/${id}/pages/${n}`; +} + +export function qrUrl(id: string): string { + return `/api/catalogs/${id}/qr`; +} + +export async function deleteCatalog(id: string, token: string): Promise { + const res = await fetch(`/api/catalogs/${id}?token=${encodeURIComponent(token)}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error(await readError(res)); +} diff --git a/client/src/components/CatalogViewer.vue b/client/src/components/CatalogViewer.vue new file mode 100644 index 0000000..5bcf717 --- /dev/null +++ b/client/src/components/CatalogViewer.vue @@ -0,0 +1,217 @@ + + + diff --git a/client/src/components/EffectSelect.vue b/client/src/components/EffectSelect.vue new file mode 100644 index 0000000..d2a07fe --- /dev/null +++ b/client/src/components/EffectSelect.vue @@ -0,0 +1,28 @@ + + + diff --git a/client/src/components/PageList.vue b/client/src/components/PageList.vue new file mode 100644 index 0000000..5aa5605 --- /dev/null +++ b/client/src/components/PageList.vue @@ -0,0 +1,28 @@ + + + diff --git a/client/src/components/effects/CssPageEffect.vue b/client/src/components/effects/CssPageEffect.vue new file mode 100644 index 0000000..27ff2cd --- /dev/null +++ b/client/src/components/effects/CssPageEffect.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/client/src/components/effects/MagazineEffect.vue b/client/src/components/effects/MagazineEffect.vue new file mode 100644 index 0000000..7e25bd3 --- /dev/null +++ b/client/src/components/effects/MagazineEffect.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/client/src/components/effects/ScrollEffect.vue b/client/src/components/effects/ScrollEffect.vue new file mode 100644 index 0000000..1c69f5d --- /dev/null +++ b/client/src/components/effects/ScrollEffect.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/client/src/constants.ts b/client/src/constants.ts new file mode 100644 index 0000000..4b76a94 --- /dev/null +++ b/client/src/constants.ts @@ -0,0 +1,9 @@ +import type { EffectId } from "@shared/types"; + +export const EFFECT_OPTIONS: { id: EffectId; title: string; hint: string }[] = [ + { id: "magazine", title: "ورق مجله", hint: "سه‌بعدی، شبیه ورق زدن کاغذ" }, + { id: "slide", title: "اسلاید افقی", hint: "صفحه بعدی از کنار می‌آید" }, + { id: "fade", title: "محو شدن", hint: "صفحه آرام عوض می‌شود" }, + { id: "card", title: "کارت", hint: "ورق دوبعدی مثل کارت" }, + { id: "scroll", title: "اسکرول عمودی", hint: "همه صفحات زیر هم" }, +]; diff --git a/client/src/env.d.ts b/client/src/env.d.ts new file mode 100644 index 0000000..54f9ed3 --- /dev/null +++ b/client/src/env.d.ts @@ -0,0 +1,21 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent; + export default component; +} + +declare module "page-flip" { + export class PageFlip { + constructor(el: HTMLElement, settings: Record); + loadFromImages(images: string[]): void; + flipNext(): void; + flipPrev(): void; + turnToPage(page: number): void; + getCurrentPageIndex(): number; + getPageCount(): number; + destroy(): void; + on(event: string, cb: (e: { data: number }) => void): void; + } +} diff --git a/client/src/images.ts b/client/src/images.ts new file mode 100644 index 0000000..3f52b77 --- /dev/null +++ b/client/src/images.ts @@ -0,0 +1,64 @@ +import { LIMITS, fileTooLargeMessage, tooManyPagesMessage } from "@shared/types"; + +const IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]); + +export function isImageFile(file: File): boolean { + if (IMAGE_TYPES.has(file.type)) return true; + return /\.(jpe?g|png|webp|gif)$/i.test(file.name); +} + +export function isPdfFile(file: File): boolean { + return file.type === "application/pdf" || /\.pdf$/i.test(file.name); +} + +function drawToJpeg(source: CanvasImageSource, width: number, height: number): Promise { + const scale = Math.min(1, LIMITS.maxPageWidth / width); + const w = Math.max(1, Math.round(width * scale)); + const h = Math.max(1, Math.round(height * scale)); + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (!ctx) return Promise.reject(new Error("تبدیل تصویر ناموفق بود")); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, w, h); + ctx.drawImage(source, 0, 0, w, h); + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (!blob) reject(new Error("تبدیل تصویر ناموفق بود")); + else resolve(blob); + }, + "image/jpeg", + LIMITS.jpegQuality / 100, + ); + }); +} + +export async function fileToJpeg(file: File): Promise { + if (file.size > LIMITS.maxFileBytes) { + throw new Error(fileTooLargeMessage(file.size)); + } + const bitmap = await createImageBitmap(file); + try { + return await drawToJpeg(bitmap, bitmap.width, bitmap.height); + } finally { + bitmap.close(); + } +} + +export async function filesToJpegs( + files: File[], + onProgress?: (done: number, total: number) => void, +): Promise { + if (files.length > LIMITS.maxPages) { + throw new Error(tooManyPagesMessage(files.length)); + } + const out: Blob[] = []; + onProgress?.(0, files.length); + for (const file of files) { + out.push(await fileToJpeg(file)); + onProgress?.(out.length, files.length); + } + return out; +} diff --git a/client/src/main.ts b/client/src/main.ts new file mode 100644 index 0000000..4ab9f8f --- /dev/null +++ b/client/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import { router } from "./router"; +import "./styles.css"; + +createApp(App).use(router).mount("#app"); diff --git a/client/src/pages/CreatePage.vue b/client/src/pages/CreatePage.vue new file mode 100644 index 0000000..6355c11 --- /dev/null +++ b/client/src/pages/CreatePage.vue @@ -0,0 +1,233 @@ + + + diff --git a/client/src/pages/DeletePage.vue b/client/src/pages/DeletePage.vue new file mode 100644 index 0000000..541dc43 --- /dev/null +++ b/client/src/pages/DeletePage.vue @@ -0,0 +1,56 @@ + + + diff --git a/client/src/pages/DonePage.vue b/client/src/pages/DonePage.vue new file mode 100644 index 0000000..48d6175 --- /dev/null +++ b/client/src/pages/DonePage.vue @@ -0,0 +1,74 @@ + + + diff --git a/client/src/pages/ViewPage.vue b/client/src/pages/ViewPage.vue new file mode 100644 index 0000000..2843160 --- /dev/null +++ b/client/src/pages/ViewPage.vue @@ -0,0 +1,47 @@ + + + diff --git a/client/src/pdf.ts b/client/src/pdf.ts new file mode 100644 index 0000000..e609922 --- /dev/null +++ b/client/src/pdf.ts @@ -0,0 +1,74 @@ +import * as pdfjs from "pdfjs-dist"; +import pdfWorker from "pdfjs-dist/build/pdf.worker.min.mjs?url"; +import { LIMITS, fileTooLargeMessage, tooManyPagesMessage } from "@shared/types"; + +pdfjs.GlobalWorkerOptions.workerSrc = pdfWorker; + +function canvasToJpeg(canvas: HTMLCanvasElement): Promise { + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => { + if (!blob) reject(new Error("تبدیل تصویر ناموفق بود")); + else resolve(blob); + }, + "image/jpeg", + LIMITS.jpegQuality / 100, + ); + }); +} + +export async function renderPdfToJpegs( + file: File, + onProgress?: (done: number, total: number) => void, +): Promise { + if (file.size > LIMITS.maxFileBytes) { + throw new Error(fileTooLargeMessage(file.size)); + } + + const data = new Uint8Array(await file.arrayBuffer()); + let pdf; + try { + pdf = await pdfjs.getDocument({ data }).promise; + } catch (err) { + const name = + err && typeof err === "object" && "name" in err + ? String((err as { name: string }).name) + : ""; + if (name === "PasswordException") { + throw new Error("این PDF رمزگذاری شده است و قابل تبدیل نیست."); + } + if (name === "InvalidPDFException") { + throw new Error("این فایل یک PDF معتبر نیست."); + } + throw err; + } + if (pdf.numPages < 1) { + throw new Error("این PDF صفحه‌ای برای نمایش ندارد."); + } + if (pdf.numPages > LIMITS.maxPages) { + throw new Error(tooManyPagesMessage(pdf.numPages)); + } + + const pages: Blob[] = []; + onProgress?.(0, pdf.numPages); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const base = page.getViewport({ scale: 1 }); + const scale = Math.min(LIMITS.maxPageWidth / base.width, 2); + const viewport = page.getViewport({ scale }); + const canvas = document.createElement("canvas"); + canvas.width = Math.floor(viewport.width); + canvas.height = Math.floor(viewport.height); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("تبدیل تصویر ناموفق بود"); + const renderTask = page.render({ + canvasContext: ctx, + canvas, + viewport, + } as Parameters[0]); + await renderTask.promise; + pages.push(await canvasToJpeg(canvas)); + onProgress?.(i, pdf.numPages); + } + return pages; +} diff --git a/client/src/router.ts b/client/src/router.ts new file mode 100644 index 0000000..6e1af94 --- /dev/null +++ b/client/src/router.ts @@ -0,0 +1,15 @@ +import { createRouter, createWebHistory } from "vue-router"; +import CreatePage from "./pages/CreatePage.vue"; +import ViewPage from "./pages/ViewPage.vue"; +import DonePage from "./pages/DonePage.vue"; +import DeletePage from "./pages/DeletePage.vue"; + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: "/", component: CreatePage }, + { path: "/c/:id/done", component: DonePage }, + { path: "/c/:id/delete", component: DeletePage }, + { path: "/c/:id", component: ViewPage }, + ], +}); diff --git a/client/src/styles.css b/client/src/styles.css new file mode 100644 index 0000000..99a6fdd --- /dev/null +++ b/client/src/styles.css @@ -0,0 +1,451 @@ +:root { + --bg: #f3eee6; + --bg-2: #fffdf9; + --ink: #1c1917; + --muted: #78716c; + --line: #e7e0d6; + --accent: #b45309; + --accent-2: #9a3412; + --danger: #b91c1c; + --stage: #161210; + --shadow: 0 18px 50px rgba(28, 25, 23, 0.12); + font-family: Vazirmatn, Tahoma, sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + min-height: 100%; + background: var(--bg); + color: var(--ink); +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +a { + color: var(--accent-2); +} + +.page { + max-width: 920px; + margin: 0 auto; + padding: 32px 20px 64px; +} + +.hero h1 { + margin: 0 0 8px; + font-size: 1.8rem; +} + +.hero p { + margin: 0; + color: var(--muted); +} + +.card { + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: 20px; + box-shadow: var(--shadow); + padding: 22px; + margin-top: 24px; +} + +.create-card { + position: relative; + overflow: hidden; +} + +.loading-overlay { + position: absolute; + inset: 0; + z-index: 5; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 24px; + background: rgba(255, 253, 249, 0.92); + text-align: center; +} + +.loading-overlay p { + margin: 0; + font-weight: 600; + color: var(--accent-2); +} + +.spinner { + width: 36px; + height: 36px; + border: 3px solid #e7e0d6; + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.progress-track { + width: min(280px, 80%); + height: 6px; + border-radius: 99px; + background: #e7e0d6; + overflow: hidden; +} + +.progress-track i { + display: block; + height: 100%; + background: var(--accent); + border-radius: 99px; + transition: width 0.2s ease; +} + +@media (prefers-reduced-motion: reduce) { + .spinner { + animation: none; + border-top-color: #e7e0d6; + border-color: var(--accent); + } +} + +.drop { + border: 2px dashed #d6cbb8; + border-radius: 16px; + padding: 36px 16px; + text-align: center; + background: #faf7f2; + transition: border-color 0.15s, background 0.15s; +} + +.drop.active { + border-color: var(--accent); + background: #fff7ed; +} + +.drop.disabled { + opacity: 0.65; + pointer-events: none; +} + +.drop input { + display: none; +} + +.effects { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + margin-top: 12px; +} + +.effect { + border: 1px solid var(--line); + border-radius: 14px; + padding: 12px; + background: #fff; + text-align: right; +} + +.effect input { + margin-left: 8px; +} + +.effect small { + display: block; + color: var(--muted); + margin-top: 4px; +} + +.flip-speed { + display: flex; + align-items: center; + gap: 12px; +} + +.flip-speed input[type="range"] { + flex: 1; +} + +.flip-speed span { + color: var(--muted); + font-size: 0.85rem; + white-space: nowrap; +} + +.pages { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.page-row { + display: flex; + align-items: center; + gap: 10px; + border: 1px solid var(--line); + border-radius: 12px; + padding: 8px; + background: #fff; +} + +.page-row img { + width: 52px; + height: 68px; + object-fit: cover; + border-radius: 6px; + background: #eee; +} + +.page-row .grow { + flex: 1; +} + +.icon-btn, +.btn { + border: 0; + border-radius: 12px; + padding: 8px 12px; + background: #e7e0d6; + color: var(--ink); +} + +.btn.primary { + background: var(--accent); + color: #fff; + padding: 12px 18px; + font-weight: 600; +} + +.btn.danger { + background: var(--danger); + color: #fff; +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.field { + margin-top: 18px; +} + +.field label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} + +.field input[type="text"] { + width: 100%; + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px 12px; + background: #fff; +} + +.error { + color: var(--danger); + margin-top: 12px; +} + +.ok { + color: #166534; +} + +.row-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 18px; +} + +.link-box { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.link-box input { + flex: 1; + border: 1px solid var(--line); + border-radius: 12px; + padding: 10px; +} + +.qr { + width: 220px; + height: 220px; + border-radius: 16px; + background: #fff; + border: 1px solid var(--line); +} + +.warn { + background: #fff7ed; + border: 1px solid #fed7aa; + color: #9a3412; + border-radius: 12px; + padding: 12px; + margin-top: 16px; +} + +.stage-page { + min-height: 100vh; + background: + radial-gradient(900px 400px at 50% -10%, rgba(180, 83, 9, 0.25), transparent 50%), + var(--stage); + color: #f5f0ea; +} + +.stage-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-bottom: 1px solid #2a241f; +} + +.stage-bar h1 { + flex: 1; + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.stage-bar a, +.stage-bar button { + color: #f5f0ea; + background: #2a241f; + text-decoration: none; +} + +.viewer-wrap { + padding: 16px; + position: relative; +} + +.viewer-wrap.cinematic { + background: + radial-gradient(1200px 500px at 50% 20%, rgba(245, 158, 11, 0.08), transparent 55%), + radial-gradient(circle at 50% 110%, rgba(0, 0, 0, 0.45), transparent 50%); + border-radius: 16px; +} + +.auto-hint { + position: relative; + max-width: 420px; + margin: 0 auto 14px; + text-align: center; + color: #fde68a; + font-size: 0.92rem; + overflow: hidden; + border-radius: 999px; + background: rgba(41, 32, 22, 0.75); + padding: 10px 16px 12px; +} + +.hint-bar { + display: block; + height: 3px; + margin: 8px 8px 0; + border-radius: 99px; + background: linear-gradient(90deg, #f59e0b, #fde68a); + transform-origin: right center; + animation: hint-fill 3s linear forwards; +} + +@keyframes hint-fill { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.stage-bar .btn.play { + background: #b45309; + color: #fff; +} + +.thumbs { + display: flex; + gap: 8px; + overflow-x: auto; + padding: 8px 16px 20px; +} + +.thumbs img { + width: 44px; + height: 58px; + object-fit: cover; + border-radius: 6px; + opacity: 0.55; + cursor: pointer; + border: 2px solid transparent; +} + +.thumbs img.active { + opacity: 1; + border-color: #f59e0b; +} + +.flip-stage { + width: min(920px, 100%); + margin: 0 auto; + height: min(72vh, 740px); + display: flex; + align-items: center; + justify-content: center; + perspective: 1600px; +} + +.flip-stage img.page-img { + max-width: 100%; + max-height: 72vh; + object-fit: contain; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); + background: #fff; + user-select: none; +} + +.scroll-list { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 820px; + margin: 0 auto; + padding: 12px; +} + +.scroll-list img { + width: 100%; + background: #fff; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3); +} + +.zoom-in img.page-img { + max-height: 92vh; + max-width: 100%; +} diff --git a/deploy/.env.production.example b/deploy/.env.production.example new file mode 100644 index 0000000..aadb304 --- /dev/null +++ b/deploy/.env.production.example @@ -0,0 +1,6 @@ +NODE_ENV=production +PORT=3020 +API_PORT=3020 +PUBLIC_BASE_URL=https://ktg.montzar.ir +STORAGE_DIR=/var/www/ktg/storage +DATABASE_PATH=/var/www/ktg/data/catalogs.sqlite diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md new file mode 100644 index 0000000..d1d20e3 --- /dev/null +++ b/deploy/DEPLOY.md @@ -0,0 +1,167 @@ +# استقرار کاتالوگ — `ktg.montzar.ir` + +هاست مشترک مونتایم: `185.50.38.53` (SSH پورت `3031`، کاربر `montime`) +دامنه: `https://ktg.montzar.ir` +پورت داخلی: `3020` (۳۰۰۰ مونتایم، ۳۰۰۱ فوری‌سند، ۳۰۱۰ سوپرادمین) + +دیتابیس Postgres بقیه پروژه‌ها را لمس نکنید. این اپ SQLite خودش را در `/var/www/ktg/data` دارد. + +--- + +## کارهایی که روی لپ‌تاپ شماست + +### ۱) بستهٔ ارسال را بسازید + +در PowerShell: + +```powershell +cd E:\projeha\DigitalCatalog +if (Test-Path catalog-deploy.tar) { Remove-Item catalog-deploy.tar } +tar --exclude=node_modules --exclude=dist --exclude=storage --exclude=data --exclude=.env --exclude=catalog-deploy.tar -cvf catalog-deploy.tar . +``` + +### ۲) آپلود روی سرور + +رمز SSH همانی است که برای سوپرادمین/مونتایم می‌زنید: + +```powershell +scp -P 3031 catalog-deploy.tar montime@185.50.38.53:/tmp/catalog-deploy.tar +``` + +بعد از آپلود، فایل `catalog-deploy.tar` روی ویندوز را می‌توانید پاک کنید. + +--- + +## کارهایی که روی سرور است + +SSH: + +```bash +ssh -p 3031 montime@185.50.38.53 +``` + +### ۳) پوشه و باز کردن فایل‌ها + +```bash +sudo mkdir -p /var/www/ktg +sudo chown montime:montime /var/www/ktg +cd /var/www/ktg +tar -xvf /tmp/catalog-deploy.tar +rm /tmp/catalog-deploy.tar +``` + +اگر پوشه از قبل چیزی داشت و آپدیت است، `storage` و `data` را پاک نکنید. + +### ۴) پورت آزاد باشد + +```bash +pm2 list +ss -tlnp | grep -E '3000|3001|3010|3020' || netstat -tlnp | grep -E '3000|3001|3010|3020' +``` + +۳۰۲۰ باید خالی باشد. اگر اشغال بود بگویید تا پورت عوض شود؛ خودتان پورت بقیه را عوض نکنید. + +### ۵) فایل `.env` سرور + +```bash +cd /var/www/ktg +cp deploy/.env.production.example .env +nano .env +``` + +باید دقیقاً این باشد (مگر پورت عوض شده باشد): + +```env +NODE_ENV=production +PORT=3020 +API_PORT=3020 +PUBLIC_BASE_URL=https://ktg.montzar.ir +STORAGE_DIR=/var/www/ktg/storage +DATABASE_PATH=/var/www/ktg/data/catalogs.sqlite +``` + +دیتابیس Postgres و `DATABASE_URL` سوپرادمین را اینجا نگذارید. + +### ۶) نصب، بیلد، پوشه‌ها + +Node ۲۰ یا جدیدتر. + +```bash +cd /var/www/ktg +npm ci +npm run build +mkdir -p storage data +``` + +### ۷) PM2 — فقط این پروسهٔ جدید + +پروسه‌های `montime` / `superadmin-api` / فوری‌سند را stop نکنید. + +```bash +pm2 start /var/www/ktg/deploy/ecosystem.config.cjs +pm2 save +pm2 list +curl -sI http://127.0.0.1:3020 +``` + +باید `HTTP/1.1 200` بیاید. اگر خطا بود: `pm2 logs ktg-catalog --lines 80` + +### ۸) Nginx — فقط سایت جدید + +```bash +sudo cp /var/www/ktg/deploy/nginx-ktg.montzar.ir.conf /etc/nginx/sites-available/ktg.montzar.ir +sudo ln -sf /etc/nginx/sites-available/ktg.montzar.ir /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +اگر `nginx -t` خطا داد، فایل‌های `adm.montzar.ir` را ویرایش نکنید؛ بگویید خروجی را بفرستید. + +### ۹) گواهی SSL + +DNS این زیردامنه از قبل مثل `adm.montzar.ir` روی همان IP است. اگر مرورگر هنوز به سرور نرسید، در پنل دامنه تأیید کنید `ktg.montzar.ir` رکورد `A` مثل `adm.montzar.ir` دارد. + +```bash +sudo certbot --nginx -d ktg.montzar.ir +``` + +Certbot خودش HTTPS را به همین vhost اضافه می‌کند، نه به سایت‌های دیگر. + +### ۱۰) تست نهایی + +1. `https://ktg.montzar.ir` صفحهٔ ساخت فارسی +2. یک PDF/تصویر کوچک بسازید +3. QR و لینک باید با `https://ktg.montzar.ir/c/...` باشد نه localhost +4. لینک مشاهده در تب دیگر باز شود + +کاتالوگ‌های لوکال (`127.0.0.1:3000`) روی این دامنه کار نمی‌کنند؛ باید روی سرور دوباره ساخته شوند. + +--- + +## آپدیت بعدی کد + +روی ویندوز دوباره مرحلهٔ ۱ و ۲. روی سرور: + +```bash +cd /var/www/ktg +# اگر tar روی فایل‌های قبلی unpacked می‌شود: +tar -xvf /tmp/catalog-deploy.tar +npm ci +npm run build +pm2 restart ktg-catalog +``` + +پوشه‌های `storage` و `data` و فایل `.env` را پاک نکنید. + +--- + +## تداخل نداشتن (یادآوری) + +| این پروژه | بقیه — دست نزنید | +|---|---| +| دامنه `ktg.montzar.ir` | `adm.montzar.ir` و دامنهٔ مونتایم | +| پورت ۳۰۲۰ | ۳۰۰۰ / ۳۰۰۱ / ۳۰۱۰ | +| PM2 نام `ktg-catalog` | پروسه‌های موجود | +| `/var/www/ktg` | `/var/www/superadmin` و پوشهٔ مونتایم | +| SQLite داخل `data/` | Postgres | + +لاگین و پروفایل اگر بعداً آمد، باز هم داخل همین دامنه و همین پوشه است؛ به دیتابیس Nest وصل نمی‌شود. diff --git a/deploy/ecosystem.config.cjs b/deploy/ecosystem.config.cjs new file mode 100644 index 0000000..966b692 --- /dev/null +++ b/deploy/ecosystem.config.cjs @@ -0,0 +1,26 @@ +/** + * PM2 — Digital Catalog on the shared montime host. + * Port 3020 avoids clash with montime (:3000), forisanad (:3001), superadmin (:3010). + * + * On server (as montime): + * pm2 start /var/www/ktg/deploy/ecosystem.config.cjs + * pm2 save + */ +module.exports = { + apps: [ + { + name: "ktg-catalog", + cwd: "/var/www/ktg", + script: "node_modules/tsx/dist/cli.mjs", + args: "--env-file=.env server/index.ts", + instances: 1, + exec_mode: "fork", + env: { + NODE_ENV: "production", + PORT: "3020", + }, + max_memory_restart: "500M", + time: true, + }, + ], +}; diff --git a/deploy/nginx-ktg.montzar.ir.conf b/deploy/nginx-ktg.montzar.ir.conf new file mode 100644 index 0000000..3c2a8a3 --- /dev/null +++ b/deploy/nginx-ktg.montzar.ir.conf @@ -0,0 +1,28 @@ +# Place under /etc/nginx/sites-available/ktg.montzar.ir +# First deploy: HTTP only. Then: sudo certbot --nginx -d ktg.montzar.ir +# +# Do not copy this into adm.montzar.ir or montime vhosts. +# Do not change client_max_body_size on other sites. + +upstream ktg_catalog { + server 127.0.0.1:3020; + keepalive 8; +} + +server { + listen 80; + listen [::]:80; + server_name ktg.montzar.ir; + + client_max_body_size 100M; + + location / { + proxy_pass http://ktg_catalog; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } +} diff --git a/docs/PROJECT.md b/docs/PROJECT.md new file mode 100644 index 0000000..43171e0 --- /dev/null +++ b/docs/PROJECT.md @@ -0,0 +1,342 @@ +# کاتالوگ دیجیتال (Digital Catalog) + +سند مرجع پروژه — ۱۸ اوت ۲۰۲۶ +قبل از پیاده‌سازی نوشته شده است. اگر رفتار محصول عوض شد، همین فایل باید به‌روز شود. + +--- + +## ۱. محصول چیست + +کاربر یک PDF چندصفحه‌ای یا چند تصویر می‌دهد. سیستم یک **کاتالوگ دیجیتال** می‌سازد: صفحات با افکت ورق‌زدن نمایش داده می‌شوند، یک **لینک مشاهده** و یک **QR** تحویل می‌شود. + +هدف دو کار با هم است: + +1. ساخت و دیدن کاتالوگ همان لحظه +2. اشتراک‌گذاری همان کاتالوگ با لینک و QR، بدون ثبت‌نام + +کاتالوگ‌ها در سایت **لیست نمی‌شوند**. فقط کسی که لینک یا QR را دارد می‌تواند ببیند. + +--- + +## ۲. تصمیم‌های قفل‌شده + +| موضوع | تصمیم | +|---|---| +| ثبت‌نام / ورود | ندارد. هر بازدیدکننده دامنه می‌تواند بسازد | +| گالری عمومی | ندارد | +| خروجی ساخت | لینک مشاهده + تصویر QR + لینک حذف | +| ویرایش بعدی | ندارد (صفحات، PDF، افکت بعد از ساخت عوض نمی‌شود) | +| انقضای خودکار | ندارد | +| زبان رابط | فارسی، راست‌به‌چپ | +| نسخه اول | ساده؛ بدون حساب کاربری، بدون پنل ادمین، بدون SEO کاتالوگ | + +--- + +## ۳. چه چیزهایی نسخه اول نیست + +این‌ها عمداً بیرون هستند تا پروژه ساده بماند: + +- حساب کاربری، نقش، پنل مدیریت +- گالری، جستجو، نقشه سایت از کاتالوگ‌ها +- ویرایش کاتالوگ بعد از ساخت +- نقطه کلیک‌پذیر روی صفحه، فهرست مطالب، ویدیو داخل صفحه +- موسیقی پس‌زمینه، پخش خودکار اجباری +- اپ دسکتاپ / موبایل بومی +- پرداخت، چندزبانه بودن رابط، تم‌های زیاد +- Three.js و شبیه‌سازی فیزیک کاغذ از صفر + +اگر فایل اشتباه آپلود شد: کاتالوگ را با لینک حذف پاک می‌کند و یکی جدید می‌سازد. + +--- + +## ۴. مسیر کاربر + +```text +ورود به دامنه + → صفحه ساخت (آپلود PDF یا چند تصویر + انتخاب افکت) + → پردازش و ذخیره + → صفحه نتیجه: + • لینک مشاهده (کپی) + • QR (نمایش + دانلود PNG) + • باز کردن کاتالوگ + • لینک حذف (یک‌بار نشان داده می‌شود) + → بیننده با لینک یا اسکن QR کاتالوگ را ورق می‌زند +``` + +لینک حذف را باید همان لحظه ذخیره کند. اگر گم شود، راه بازیابی نیست (چون حساب کاربری نداریم). این محدودیت باید روی صفحه نتیجه نوشته شود. + +--- + +## ۵. امکانات نسخه اول + +### ساخت + +- Drag & drop یا انتخاب فایل +- هنگام خواندن PDF/تصویر، آماده‌سازی صفحات و ساخت کاتالوگ، لودینگ با متن پیشرفت نشان داده می‌شود +- یک PDF **یا** چند تصویر (نه هر دو با هم در یک کاتالوگ) +- ترتیب تصاویر = ترتیب انتخاب / ترتیب نام فایل؛ امکان جابه‌جایی قبل از ساخت +- انتخاب یکی از افکت‌های ورق‌زدن +- تعیین زمان ورق‌خوردن (۴۰۰ تا ۵۰۰۰ میلی‌ثانیه؛ پیش‌فرض ۱ ثانیه). بعد از ساخت عوض نمی‌شود +- عنوان اختیاری (اگر خالی باشد: «کاتالوگ») + +### مشاهده + +- نمایش صفحات با افکت انتخاب‌شده (اسلاید، محو و کارت با انتقال ورودی/خروجی واقعی) +- ۳ ثانیه پس از باز شدن، پخش خودکار شروع می‌شود؛ فاصلهٔ بین ورق‌ها برابر زمان ورق‌خوردن ذخیره‌شده به‌علاوهٔ کمی مکث است +- قبلی / بعدی، کلیدهای جهت، کلیک/اسوایپ روی صفحه +- بندانگشتی صفحات +- زوم و تمام‌صفحه +- دسکتاپ: فقط افکت `magazine` دو صفحه روبه‌رو نشان می‌دهد؛ بقیه همیشه یک صفحه +- موبایل: همیشه یک صفحه؛ اگر ورق سه‌بعدی روی دستگاه ضعیف بود، همان کاتالوگ با اسلاید افقی دیده می‌شود (تنظیم ذخیره‌شده عوض نمی‌شود) + +### اشتراک + +- آدرس ثابت: `{BASE_URL}/c/{id}` +- QR همان آدرس را کد می‌کند +- دانلود QR به صورت PNG + +### حذف + +- حذف فقط با توکن مخفی +- بعد از حذف، لینک و QR دیگر کار نمی‌کنند +- فایل صفحات از دیسک پاک می‌شوند + +--- + +## ۶. افکت‌های ورق‌زدن + +موقع ساخت، **یکی** انتخاب می‌شود و برای همیشه روی همان کاتالوگ می‌ماند. + +| شناسه | نام نمایشی | رفتار | +|---|---|---| +| `magazine` | ورق مجله | سه‌بعدی واقعی؛ افکت اصلی | +| `slide` | اسلاید افقی | صفحه بعدی از بغل می‌آید | +| `fade` | محو شدن | صفحه فعلی محو، بعدی ظاهر می‌شود | +| `card` | کارت | ورق دوبعدی مثل کارت | +| `scroll` | اسکرول عمودی | صفحات زیر هم؛ مناسب موبایل | + +پیاده‌سازی: برای `magazine` از کتابخانه **StPageFlip** استفاده می‌شود. بقیه با Vue + CSS/transition. فیزیک کاغذ از صفر نوشته نمی‌شود. + +--- + +## ۷. معماری + +یک اپلیکیشن با دو بخش در یک مخزن: + +- **فرانت‌اند:** Vue 3 — ساخت، نتیجه، نمایش کاتالوگ +- **بک‌اند:** API سبک Node — آپلود، ذخیره، سرو کردن صفحات، QR، حذف + +کاتالوگ‌ها روی سرور ذخیره می‌شوند تا لینک برای همه باز شود. تبدیل PDF به تصویر **در مرورگر با pdf.js** انجام می‌شود. سرور PDF خام نمی‌گیرد؛ فقط JPEG صفحات را ذخیره می‌کند. به ابزار native مثل Poppler نیاز نیست. + +```text +مرورگر سرور +────── ──── +انتخاب PDF/تصویر +pdf.js → تصویر هر صفحه +آپلود تصاویر + افکت + عنوان → ذخیره فایل‌ها روی دیسک + → ردیف در SQLite + → ساخت QR +صفحه نتیجه ← لینک/QR/حذف +بیننده /c/:id → متادیتا + آدرس تصاویر +نمایش با افکت انتخاب‌شده +``` + +--- + +## ۸. استک قفل‌شده + +| لایه | انتخاب | دلیل | +|---|---|---| +| UI | Vue 3 + Vite + TypeScript | کامپوننت و state برای آپلود و نمایش | +| استایل | CSS ساده + RTL | بدون UI kit سنگین | +| PDF در مرورگر | pdf.js | تبدیل صفحه به تصویر بدون سرور خاص | +| ورق مجله | StPageFlip | افکت آماده و قابل‌نگهداری | +| QR | بسته `qrcode` در سرور | PNG قابل دانلود | +| API | Hono روی Node | سبک، TypeScript | +| داده | SQLite از طریق sql.js + Drizzle | بدون کامپایل native (روی ویندوز بدون Visual Studio هم نصب می‌شود) | +| فایل صفحات | پوشه محلی دیسک | ساده برای یک سرور | +| شناسه | nanoid | کوتاه و غیرقابل حدس نسبی | + +HTML/CSS خالی رد شد: آپلود، تبدیل PDF، چند افکت، موبایل و صفحه نتیجه خیلی زود شلوغ می‌شود. + +Nuxt و فریم‌ورک سنگین بک‌اند رد شد: نمایش کاتالوگ تقریباً همه در کلاینت است؛ یک API کوچک کافی است. + +--- + +## ۹. مدل داده + +جدول `catalogs`: + +| فیلد | نوع | توضیح | +|---|---|---| +| `id` | متن، حدود ۱۰ کاراکتر | شناسه عمومی در لینک | +| `delete_token` | متن، حدود ۲۴ کاراکتر | فقط برای حذف؛ در URL مشاهده نیست | +| `title` | متن | عنوان | +| `effect` | متن | یکی از شناسه‌های بخش ۶ | +| `page_count` | عدد | تعداد صفحات ذخیره‌شده | +| `flip_ms` | عدد | مدت انیمیشن ورق‌خوردن به میلی‌ثانیه (۴۰۰–۵۰۰۰) | +| `created_at` | زمان | | + +فایل‌ها: + +```text +storage/catalogs/{id}/ + page-001.jpg + page-002.jpg + ... + qr.png +``` + +صفحات با عرض حداکثر ۱۶۰۰ پیکسل، JPEG با کیفیت حدود ۸۵ ذخیره می‌شوند. + +--- + +## ۱۰. API + +پایه: `/api` + +| متد | مسیر | کار | +|---|---|---| +| `POST` | `/catalogs` | ساخت. بدنه: تصاویر JPEG صفحات (نه خود PDF) + `title` + `effect` + `flipMs` | +| `GET` | `/catalogs/:id` | متادیتا برای بیننده (بدون `delete_token`)؛ شامل `flipMs` | +| `GET` | `/catalogs/:id/pages/:n` | فایل تصویر صفحه | +| `GET` | `/catalogs/:id/qr` | PNG کد QR. عمومی است چون فقط همان لینک مشاهده را کد می‌کند | +| `DELETE` | `/catalogs/:id` | حذف؛ کوئری `token` باید با `delete_token` یکی باشد | + +پاسخ ساخت: + +```json +{ + "id": "abc12xyz90", + "viewUrl": "https://example.com/c/abc12xyz90", + "deleteUrl": "https://example.com/c/abc12xyz90/delete?token=...", + "qrUrl": "https://example.com/api/catalogs/abc12xyz90/qr" +} +``` + +`deleteUrl` فقط همین یک‌بار به سازنده داده می‌شود. از `GET` عمومی برنمی‌گردد. + +صفحات فرانت: + +| مسیر | صفحه | +|---|---| +| `/` | ساخت کاتالوگ | +| `/c/:id` | نمایش کاتالوگ | +| `/c/:id/done?token=...` | صفحه نتیجه (لینک، QR، حذف) — توکن فقط برای نشان دادن دکمه حذف و جلوگیری از دیدن این صفحه توسط بیننده معمولی | +| `/c/:id/delete?token=...` | تأیید حذف | + +بیننده `/c/:id` را می‌بیند. صفحه نتیجه بدون توکن باز نشود. + +--- + +## ۱۱. محدودیت‌ها (ضد سوءاستفاده) + +چون ثبت‌نام نیست، این سقف‌ها اجباری‌اند: + +| مورد | سقف نسخه اول | +|---|---| +| حجم هر فایل ورودی | ۱۰۰ مگابایت | +| تعداد صفحه | حداکثر ۲۰۰ | +| فرمت PDF | `application/pdf` | +| فرمت تصویر | jpeg, png, webp, gif | +| ساخت از یک IP | ۱۰ کاتالوگ در ساعت (شمارنده در حافظه همان پروسه Node؛ یک سرور) | +| عرض تصویر ذخیره‌شده | حداکثر ۱۶۰۰px | +| زمان ورق‌خوردن | ۴۰۰ تا ۵۰۰۰ میلی‌ثانیه | + +اگر سقف رد شود، پیام فارسی واضح نشان داده می‌شود و چیزی ذخیره نمی‌شود. + +--- + +## ۱۲. امنیت و حریم + +- کاتالوگ **unlisted** است: endpoint لیست وجود ندارد +- `id` تصادفی است؛ نباید پشت سر هم و قابل حدس باشد +- حذف فقط با `delete_token` +- نوع فایل واقعی بررسی شود، نه فقط پسوند +- فایل آپلودی هرگز به عنوان اسکریپت سرو نشود +- برای QR باید `PUBLIC_BASE_URL` در محیط سرور تنظیم شود تا لینک داخل QR درست باشد + +این مدل «امنیت از طریق مخفی بودن لینک» است، نه قفل رمز روی کاتالوگ. هر کس لینک را داشته باشد محتوا را می‌بیند. + +--- + +## ۱۳. رابط کاربری (خلاصه) + +سه صفحه اصلی، فارسی و RTL: + +1. **ساخت:** ناحیه آپلود، لیست صفحات (مرتب‌سازی)، انتخاب افکت، دکمه ساخت +2. **نتیجه:** لینک، QR، هشدار «لینک حذف را ذخیره کن»، دکمه حذف +3. **نمایش:** استیج کاتالوگ، کنترل‌ها، بندانگشتی + +ظاهر نسخه اول تمیز و ساده باشد، نه داشبورد شلوغ. + +--- + +## ۱۴. اجرا و پیکربندی + +یک سرور Node کافی است. SQLite و پوشه `storage/` کنار اپ می‌مانند. + +متغیرهای محیط: + +```text +PORT=3000 +API_PORT=3010 +PUBLIC_BASE_URL=https://ktg.montzar.ir +STORAGE_DIR=./storage +DATABASE_PATH=./data/catalogs.sqlite +``` + +`PUBLIC_BASE_URL` همان دامنه‌ای است که داخل QR نوشته می‌شود. + +--- + +## ۱۵. گزینه‌هایی که بررسی شد و انتخاب نشد + +1. **فقط HTML/CSS** — برای دمو کافی است؛ برای آپلود، PDF، چند افکت و لینک عمومی کافی نیست. +2. **فقط Vue در مرورگر، بدون سرور** — کاتالوگ بعد از بستن تب از بین می‌رود؛ لینک و QR برای دیگری کار نمی‌کند. +3. **Vue + بک‌اند + حساب کاربری / گالری** — برای نسخه اول اضافه است. + +انتخاب نهایی: **Vue 3 + API سبک Node + SQLite + فایل روی دیسک.** + +--- + +## ۱۶. معیار تمام شدن نسخه اول + +نسخه اول وقتی تمام است که: + +- [x] با چند تصویر کاتالوگ ساخته شود (تست API با JPEG) +- [ ] با PDF چندصفحه‌ای در مرورگر (نیاز به بررسی دستی) +- [x] پنج افکت در UI پیاده شده‌اند (بررسی ظاهری باقی است) +- [x] لینک `/c/:id` از API متادیتا می‌گیرد +- [x] QR همان لینک را کد می‌کند +- [x] حذف با توکن کاتالوگ را از بین ببرد +- [x] بدون توکن نشود حذف کرد +- [x] گالری یا لیست عمومی وجود نداشته باشد +- [ ] روی موبایل بررسی دستی +- [x] سقف تعداد صفحه پیام خطا بدهد + +--- + +## ۱۷. کارهای بعدی (عمداً بعد از نسخه اول) + +فقط اگر بعداً لازم شد، نه الان: + +- ذخیره لینک‌های همین مرورگر در `localStorage` +- عوض کردن افکت بدون آپلود دوباره +- انقضای اختیاری +- رمز روی کاتالوگ +- هاست چند سروره / S3 +- حساب کاربری برای تیم + +--- + +## تاریخچه تصمیم + +| تاریخ | تصمیم | +|---|---| +| ۲۰۲۶-۰۸-۱۸ | محصول: کاتالوگ دیجیتال از PDF/تصویر با افکت و اشتراک | +| ۲۰۲۶-۰۸-۱۸ | از اول هم مشاهده همان‌جا، هم لینک عمومی + QR | +| ۲۰۲۶-۰۸-۱۸ | بدون ورود؛ هر بازدیدکننده می‌تواند بسازد | +| ۲۰۲۶-۰۸-۱۸ | بدون گالری؛ فقط لینک و QR | +| ۲۰۲۶-۰۸-۱۸ | بعد از ساخت: لینک + QR + لینک حذف؛ بدون ویرایش و بدون انقضا | +| ۲۰۲۶-۰۸-۱۸ | استک: Vue 3 + Hono + SQLite + pdf.js + StPageFlip | diff --git a/docs/superpowers/plans/2026-08-18-digital-catalog.md b/docs/superpowers/plans/2026-08-18-digital-catalog.md new file mode 100644 index 0000000..8f35250 --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-digital-catalog.md @@ -0,0 +1,366 @@ +# Digital Catalog Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Persian RTL web app where anyone can upload a PDF or images, get a shareable flipbook catalog with a public link and QR code, and delete it only with a one-time secret token. + +**Architecture:** Vue 3 + Vite client converts PDF pages to JPEG in the browser with pdf.js, then POSTs images to a Hono API. The API stores JPEGs on disk, metadata in SQLite, and a QR PNG. Vite proxies `/api` in development; production Hono serves `dist/` plus the API. Catalogs are unlisted (no listing endpoint). + +**Tech Stack:** Vue 3, Vue Router, TypeScript, Vite, pdf.js (`pdfjs-dist`), StPageFlip (`page-flip`), Hono, `@hono/node-server`, Drizzle ORM, better-sqlite3, nanoid, qrcode, sharp, Vitest. + +## Global Constraints + +- UI language: Persian, `dir="rtl"` +- No auth, no public gallery, no post-create edit, no auto-expiry +- After create: view link + QR + delete link only +- Effects (locked ids): `magazine` | `slide` | `fade` | `card` | `scroll` +- Max input file 25MB, max 80 pages, 10 catalogs/hour/IP (in-memory) +- Stored pages: JPEG, max width 1600px, quality ~85 +- Public URL shape: `{PUBLIC_BASE_URL}/c/{id}` +- POST `/api/catalogs` accepts JPEG page images only (never raw PDF) +- `delete_token` never returned from public GET +- No git commits unless the user explicitly asks +- Spec source of truth: `docs/PROJECT.md` + +## File Map + +``` +package.json +tsconfig.json +tsconfig.app.json +tsconfig.node.json +vite.config.ts +vitest.config.ts +.env.example +.gitignore +README.md +server/env.ts +server/app.ts +server/index.ts +server/db.ts +server/schema.ts +server/catalog-repo.ts +server/storage.ts +server/rate-limit.ts +server/qr.ts +server/magic.ts +server/errors.ts +server/routes/catalogs.ts +shared/types.ts +client/index.html +client/src/main.ts +client/src/App.vue +client/src/styles.css +client/src/router.ts +client/src/api.ts +client/src/pdf.ts +client/src/images.ts +client/src/constants.ts +client/src/pages/CreatePage.vue +client/src/pages/ViewPage.vue +client/src/pages/DonePage.vue +client/src/pages/DeletePage.vue +client/src/components/EffectSelect.vue +client/src/components/PageList.vue +client/src/components/CatalogViewer.vue +client/src/components/effects/MagazineEffect.vue +client/src/components/effects/SlideEffect.vue +client/src/components/effects/FadeEffect.vue +client/src/components/effects/CardEffect.vue +client/src/components/effects/ScrollEffect.vue +tests/server/helpers.ts +tests/server/rate-limit.test.ts +tests/server/storage.test.ts +tests/server/catalogs.test.ts +``` + +## Shared types (all later tasks use these) + +```ts +export const EFFECTS = ["magazine", "slide", "fade", "card", "scroll"] as const; +export type EffectId = (typeof EFFECTS)[number]; + +export type CatalogPublic = { + id: string; + title: string; + effect: EffectId; + pageCount: number; + createdAt: string; +}; + +export type CreateCatalogResponse = { + id: string; + viewUrl: string; + deleteUrl: string; + qrUrl: string; + doneUrl: string; +}; +``` + +--- + +### Task 1: Scaffold + shared types + env + +**Files:** +- Create: `package.json`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`, `vite.config.ts`, `vitest.config.ts`, `.env.example`, `.gitignore`, `shared/types.ts`, `server/env.ts` + +**Interfaces:** +- Consumes: nothing +- Produces: `loadEnv()` returning `{ port, publicBaseUrl, storageDir, databasePath }` + +- [ ] **Step 1:** Create `shared/types.ts` with the types above and `server/env.ts` that reads `PORT`, `PUBLIC_BASE_URL`, `STORAGE_DIR`, `DATABASE_PATH` with defaults `3000`, `http://localhost:3000`, `./storage`, `./data/catalogs.sqlite`. +- [ ] **Step 2:** Create Vite config with `root: "client"`, `server.proxy["/api"] = "http://localhost:3001"`, and Vitest including `tests/**/*.test.ts`. +- [ ] **Step 3:** Install dependencies listed in Tech Stack plus `concurrently`, `tsx`, `vue-router`, `drizzle-kit`. Scripts: `dev` (API on 3001 + Vite), `test`, `build`, `start`. +- [ ] **Step 4:** `npx vitest run` should succeed with zero tests or a trivial env test. Do not commit. + +--- + +### Task 2: Rate limiter + +**Files:** +- Create: `server/rate-limit.ts`, `tests/server/rate-limit.test.ts` + +**Interfaces:** +- Consumes: nothing +- Produces: `createRateLimiter(limit: number, windowMs: number)` with `check(ip: string): boolean` (true = allowed) + +- [ ] **Step 1: Write failing tests** in `tests/server/rate-limit.test.ts`: + - allows first 10 hits + - rejects the 11th within the window + - allows again after window elapses (injectable clock) +- [ ] **Step 2:** Run `npx vitest run tests/server/rate-limit.test.ts` — expect FAIL (module missing). +- [ ] **Step 3:** Implement in-memory sliding window in `server/rate-limit.ts`. +- [ ] **Step 4:** Re-run tests — expect PASS. + +--- + +### Task 3: Storage + magic bytes + +**Files:** +- Create: `server/storage.ts`, `server/magic.ts`, `tests/server/storage.test.ts` + +**Interfaces:** +- Consumes: `storageDir` from env +- Produces: + - `assertJpeg(buffer: Buffer): void` throws if not JPEG + - `saveCatalogPages(id: string, pages: Buffer[]): Promise` writes `page-001.jpg`… + - `saveQr(id: string, png: Buffer): Promise` + - `readPage(id: string, n: number): Promise` + - `readQr(id: string): Promise` + - `deleteCatalogFiles(id: string): Promise` + +- [ ] **Step 1:** Write tests using a temp dir: save 2 tiny JPEGs, read page 1, delete, then read throws. +- [ ] **Step 2:** Run tests — expect FAIL. +- [ ] **Step 3:** Implement filesystem helpers. Use sharp to re-encode JPEG max width 1600, quality 85. +- [ ] **Step 4:** Tests PASS. + +--- + +### Task 4: SQLite catalog repository + +**Files:** +- Create: `server/schema.ts`, `server/db.ts`, `server/catalog-repo.ts` + +**Interfaces:** +- Consumes: `databasePath` +- Produces: + - `createCatalog({ id, deleteToken, title, effect, pageCount }): CatalogRow` + - `getById(id): CatalogRow | null` (includes `deleteToken` internally) + - `deleteById(id): boolean` + +```ts +export type CatalogRow = { + id: string; + deleteToken: string; + title: string; + effect: EffectId; + pageCount: number; + createdAt: string; +}; +``` + +- [ ] **Step 1:** Tests: insert, getById, delete, getById returns null. Use temp sqlite file. +- [ ] **Step 2:** Run — FAIL. +- [ ] **Step 3:** Drizzle schema `catalogs` with columns `id`, `delete_token`, `title`, `effect`, `page_count`, `created_at`. Auto-create table on boot. +- [ ] **Step 4:** Tests PASS. + +--- + +### Task 5: Catalog HTTP API + +**Files:** +- Create: `server/app.ts`, `server/routes/catalogs.ts`, `server/qr.ts`, `server/errors.ts`, `server/index.ts`, `tests/server/helpers.ts`, `tests/server/catalogs.test.ts` + +**Interfaces:** +- Consumes: repo, storage, rate limiter, `PUBLIC_BASE_URL` +- Produces Hono app with routes under `/api`: + +| Method | Path | Behavior | +|---|---|---| +| POST | `/catalogs` | multipart: `title`, `effect`, `pages` (1..80 JPEGs). Rate-limit by `x-forwarded-for` or socket IP. Returns JSON `{ id, viewUrl, deleteUrl, qrUrl, doneUrl }` | +| GET | `/catalogs/:id` | `{ id, title, effect, pageCount, createdAt }` — never `deleteToken` | +| GET | `/catalogs/:id/pages/:n` | JPEG bytes, `Content-Type: image/jpeg` | +| GET | `/catalogs/:id/qr` | PNG bytes | +| DELETE | `/catalogs/:id?token=` | 403 if token wrong, 404 if missing, 204 + delete files if ok | + +Error JSON: `{ error: string }` with Persian messages: +- حجم یا تعداد صفحه بیش از حد مجاز است +- نوع فایل پشتیبانی نمی‌شود +- افکت نامعتبر است +- کاتالوگ پیدا نشد +- اجازه حذف ندارید +- تعداد ساخت از این آدرس بیش از حد مجاز است + +Limits: each page buffer ≤ 25MB; page count 1–80; `effect` in EFFECTS; title default `کاتالوگ`; `id` nanoid(10); `deleteToken` nanoid(24). + +QR encodes `{publicBaseUrl}/c/{id}`. + +`viewUrl` = `{publicBaseUrl}/c/{id}` +`deleteUrl` = `{publicBaseUrl}/c/{id}/delete?token={deleteToken}` +`doneUrl` = `{publicBaseUrl}/c/{id}/done?token={deleteToken}` +`qrUrl` = `/api/catalogs/{id}/qr` + +- [ ] **Step 1:** Tests with `app.request()`: + 1. POST 2 tiny JPEGs + effect magazine → 201, body has id/urls, GET public has no deleteToken + 2. GET page 1 is JPEG + 3. GET qr is PNG + 4. DELETE without token → 403, catalog still GET 200 + 5. DELETE with token → 204, then GET 404 + 6. POST 81 pages → 400 + 7. POST invalid effect → 400 + 8. POST non-jpeg → 400 + 9. 11th POST from same IP in window → 429 + 10. No GET `/api/catalogs` list route (404) +- [ ] **Step 2:** Run — FAIL. +- [ ] **Step 3:** Implement routes. `server/index.ts` listens 3001 in dev (`PORT_API` default 3001) and in production listens `PORT` serving `../dist` static files. +- [ ] **Step 4:** Tests PASS. + +--- + +### Task 6: Vue shell + router + RTL + +**Files:** +- Create: `client/index.html`, `client/src/main.ts`, `client/src/App.vue`, `client/src/styles.css`, `client/src/router.ts`, `client/src/constants.ts`, `client/src/api.ts` + +Routes: +- `/` CreatePage +- `/c/:id` ViewPage +- `/c/:id/done` DonePage (requires `token` query; if missing, redirect to view) +- `/c/:id/delete` DeletePage (requires `token`) + +`api.ts`: +- `createCatalog({ title, effect, pages: Blob[] }): Promise` +- `getCatalog(id): Promise` +- `pageUrl(id, n): string` → `/api/catalogs/${id}/pages/${n}` +- `qrUrl(id): string` +- `deleteCatalog(id, token): Promise` + +- [ ] **Step 1:** App boots, Persian RTL, `/` shows a heading «کاتالوگ دیجیتال». +- [ ] **Step 2:** `npm run dev` starts without error. Do not commit. + +--- + +### Task 7: Client PDF/image pipeline + +**Files:** +- Create: `client/src/pdf.ts`, `client/src/images.ts` + +**Interfaces:** +- `renderPdfToJpegs(file: File, onProgress?: (done: number, total: number) => void): Promise` + - reject if > 25MB + - reject if page count > 80 + - render scale so width ≤ 1600, JPEG quality 0.85 +- `filesToJpegs(files: File[]): Promise` for jpeg/png/webp/gif, same size/count limits, preserve chosen order + +- [ ] **Step 1:** Wire pdfjs worker from `pdfjs-dist`. +- [ ] **Step 2:** Create page can convert a 1-page PDF and 2 images (manual check later with UI). + +--- + +### Task 8: Create page + +**Files:** +- Create: `client/src/pages/CreatePage.vue`, `client/src/components/EffectSelect.vue`, `client/src/components/PageList.vue` + +Behavior: +- Drag/drop or file picker +- One PDF **or** multiple images, not mixed; if mixed, Persian error +- Reorder pages (up/down) +- Effect radios with Persian labels from spec section 6 +- Optional title +- Submit → POST → `router.push(/c/{id}/done?token=...)` +- Show progress while rendering PDF +- Show API errors in Persian + +- [ ] **Step 1:** Implement UI, keep it clean (not a busy dashboard). +- [ ] **Step 2:** Manual: drop images, pick effect, submit reaches API. + +--- + +### Task 9: Done + delete pages + +**Files:** +- Create: `client/src/pages/DonePage.vue`, `client/src/pages/DeletePage.vue` + +Done: +- Copy view link `{origin}/c/{id}` +- Show QR `` +- Download QR PNG +- Open catalog button +- Warning: «لینک حذف را همین حالا ذخیره کنید. اگر گم شود قابل بازیابی نیست.» +- Link to delete page with token + +Delete: +- Confirm button «حذف کاتالوگ» +- On success, message that it is gone; view link no longer works + +- [ ] **Step 1:** Implement. +- [ ] **Step 2:** Verify token-less `/done` redirects to view. + +--- + +### Task 10: Viewer + five effects + +**Files:** +- Create: viewer components listed in File Map, `client/src/pages/ViewPage.vue` + +Behavior from spec: +- Load catalog; 404 Persian if missing +- Thumbnails, prev/next, arrow keys, fullscreen, zoom +- `magazine`: StPageFlip, two-page spread on desktop (`window.innerWidth >= 900`), single page on mobile; if PageFlip throws or device is coarse+narrow, fall back to SlideEffect visually without changing stored effect +- `slide` / `fade` / `card`: one page, CSS transitions +- `scroll`: all pages stacked, vertical scroll +- Click page edge to turn for magazine/slide/card/fade + +- [ ] **Step 1:** Implement MagazineEffect with `page-flip`. +- [ ] **Step 2:** Implement the four CSS effects. +- [ ] **Step 3:** Keyboard and thumbnails work. + +--- + +### Task 11: Production serve + README + spec checklist + +**Files:** +- Modify: `server/index.ts`, `README.md`, `docs/PROJECT.md` (checkboxes in section 16 stay until verified) + +- [ ] **Step 1:** Production: `vite build` then Hono on `PORT` serves `dist` with SPA fallback for `/c/:id` routes, API still at `/api`. +- [ ] **Step 2:** README: how to copy `.env.example`, `npm install`, `npm run dev`, `npm test`, `npm run build` / `npm start`. +- [ ] **Step 3:** Run `npx vitest run` — all pass. +- [ ] **Step 4:** Smoke: create from images, open `/c/:id`, QR file exists, delete with token, GET 404. + +--- + +## Spec coverage (self-review) + +| Spec section | Task | +|---|---| +| Unlisted, no auth, link+QR+delete | 5, 8, 9 | +| Five effects | 10 | +| pdf.js in browser, JPEG to server | 5, 7 | +| Limits 25MB / 80 pages / 10 per hour | 5, 7 | +| SQLite + disk layout | 3, 4 | +| Persian RTL | 6–10 | +| Magazine spread desktop / single mobile | 10 | +| No gallery endpoint | 5 test 10 | +| PUBLIC_BASE_URL in QR | 5 | +| Done page requires token | 6, 9 | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9884373 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4369 @@ +{ + "name": "digital-catalog", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "digital-catalog", + "version": "1.0.0", + "dependencies": { + "@hono/node-server": "^2.1.1", + "drizzle-orm": "^0.45.2", + "hono": "^4.13.3", + "nanoid": "^5.1.16", + "page-flip": "^2.0.7", + "pdfjs-dist": "^4.10.38", + "qrcode": "^1.5.4", + "sql.js": "^1.14.1", + "tsx": "^4.19.2", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "@types/qrcode": "^1.5.6", + "@types/sql.js": "^1.4.11", + "@vitejs/plugin-vue": "^5.2.1", + "concurrently": "^9.1.2", + "typescript": "^5.7.3", + "vite": "^6.0.7", + "vitest": "^2.1.8", + "vue-tsc": "^2.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmmirror.com/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sql.js": { + "version": "1.4.11", + "resolved": "https://registry.npmmirror.com/@types/sql.js/-/sql.js-1.4.11.tgz", + "integrity": "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/emscripten": "*", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.0.tgz", + "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^0.4.9", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "0.4.14", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-0.4.14.tgz", + "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/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/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "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/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/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/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/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/concurrently/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/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/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hono": { + "version": "4.13.3", + "resolved": "https://registry.npmmirror.com/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/page-flip": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/page-flip/-/page-flip-2.0.7.tgz", + "integrity": "sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA==", + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.10.38", + "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.65" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/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/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/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/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/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/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.0.7", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "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": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.0.tgz", + "integrity": "sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.0" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dd20dfa --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "digital-catalog", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "concurrently -n api,web -c blue,green \"npm run dev:api\" \"npm run dev:web\"", + "dev:api": "tsx --env-file=.env server/index.ts", + "dev:web": "vite", + "build": "vite build", + "start": "tsx --env-file=.env server/index.ts", + "test": "vitest run" + }, + "dependencies": { + "@hono/node-server": "^2.1.1", + "drizzle-orm": "^0.45.2", + "hono": "^4.13.3", + "nanoid": "^5.1.16", + "page-flip": "^2.0.7", + "pdfjs-dist": "^4.10.38", + "qrcode": "^1.5.4", + "sql.js": "^1.14.1", + "tsx": "^4.19.2", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "@types/qrcode": "^1.5.6", + "@types/sql.js": "^1.4.11", + "@vitejs/plugin-vue": "^5.2.1", + "concurrently": "^9.1.2", + "typescript": "^5.7.3", + "vite": "^6.0.7", + "vitest": "^2.1.8", + "vue-tsc": "^2.2.0" + } +} diff --git a/server/app.ts b/server/app.ts new file mode 100644 index 0000000..e25490a --- /dev/null +++ b/server/app.ts @@ -0,0 +1,44 @@ +import { Hono } from "hono"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { LIMITS } from "../shared/types"; +import { createCatalogRepo } from "./catalog-repo"; +import { openDatabase } from "./db"; +import type { Env } from "./env"; +import { HttpError } from "./errors"; +import { createRateLimiter } from "./rate-limit"; +import { catalogRoutes } from "./routes/catalogs"; + +export async function createApp(env: Env, options?: { now?: () => number }) { + const db = await openDatabase(env.databasePath); + const repo = createCatalogRepo(db); + const limiter = createRateLimiter( + LIMITS.maxCreatesPerHour, + LIMITS.rateWindowMs, + options?.now, + ); + + const app = new Hono(); + + app.onError((err, c) => { + if (err instanceof HttpError) { + return c.json({ error: err.message }, err.status as 400); + } + console.error(err); + return c.json({ error: "خطای داخلی سرور" }, 500); + }); + + app.route("/api", catalogRoutes({ env, repo, limiter })); + + if (env.isProduction) { + const dist = "./dist"; + app.use("/*", serveStatic({ root: dist })); + app.get("*", async (c) => { + const html = await readFile(path.join("dist", "index.html"), "utf8"); + return c.html(html); + }); + } + + return app; +} diff --git a/server/catalog-repo.ts b/server/catalog-repo.ts new file mode 100644 index 0000000..707aa22 --- /dev/null +++ b/server/catalog-repo.ts @@ -0,0 +1,79 @@ +import { eq } from "drizzle-orm"; +import type { EffectId } from "../shared/types"; +import { DEFAULT_FLIP_MS } from "../shared/types"; +import type { DbBundle } from "./db"; +import { catalogsTable } from "./schema"; + +export type CatalogRow = { + id: string; + deleteToken: string; + title: string; + effect: EffectId; + pageCount: number; + flipMs: number; + createdAt: string; +}; + +export type CatalogInsert = Omit & { + createdAt?: string; + flipMs?: number; +}; + +function toRow(record: typeof catalogsTable.$inferSelect): CatalogRow { + return { + id: record.id, + deleteToken: record.deleteToken, + title: record.title, + effect: record.effect as EffectId, + pageCount: record.pageCount, + flipMs: record.flipMs ?? DEFAULT_FLIP_MS, + createdAt: record.createdAt, + }; +} + +export function createCatalogRepo(db: DbBundle) { + return { + create(input: CatalogInsert): CatalogRow { + const createdAt = input.createdAt ?? new Date().toISOString(); + const flipMs = input.flipMs ?? DEFAULT_FLIP_MS; + db.drizzle.insert(catalogsTable).values({ + id: input.id, + deleteToken: input.deleteToken, + title: input.title, + effect: input.effect, + pageCount: input.pageCount, + flipMs, + createdAt, + }).run(); + db.persist(); + return { + id: input.id, + deleteToken: input.deleteToken, + title: input.title, + effect: input.effect, + pageCount: input.pageCount, + flipMs, + createdAt, + }; + }, + + getById(id: string): CatalogRow | null { + const row = db.drizzle + .select() + .from(catalogsTable) + .where(eq(catalogsTable.id, id)) + .get(); + return row ? toRow(row) : null; + }, + + deleteById(id: string): boolean { + const existing = this.getById(id); + if (!existing) return false; + db.drizzle.delete(catalogsTable).where(eq(catalogsTable.id, id)).run(); + db.persist(); + return true; + }, + }; +} + +export type CatalogRepo = ReturnType; diff --git a/server/db.ts b/server/db.ts new file mode 100644 index 0000000..327d2cb --- /dev/null +++ b/server/db.ts @@ -0,0 +1,58 @@ +import fs from "node:fs"; +import path from "node:path"; +import { createRequire } from "node:module"; +import initSqlJs, { type Database as SqlJsDatabase } from "sql.js"; +import { drizzle, type SQLJsDatabase } from "drizzle-orm/sql-js"; +import { catalogsTable } from "./schema"; + +const require = createRequire(import.meta.url); + +export type DbBundle = { + sqlite: SqlJsDatabase; + drizzle: SQLJsDatabase; + persist: () => void; +}; + +export async function openDatabase(databasePath: string): Promise { + fs.mkdirSync(path.dirname(databasePath), { recursive: true }); + + const sqlJsDist = path.dirname(require.resolve("sql.js/dist/sql-wasm.js")); + const SQL = await initSqlJs({ + locateFile: (file) => path.join(sqlJsDist, file), + }); + + const sqlite = fs.existsSync(databasePath) + ? new SQL.Database(fs.readFileSync(databasePath)) + : new SQL.Database(); + + sqlite.run(` + CREATE TABLE IF NOT EXISTS catalogs ( + id TEXT PRIMARY KEY, + delete_token TEXT NOT NULL, + title TEXT NOT NULL, + effect TEXT NOT NULL, + page_count INTEGER NOT NULL, + flip_ms INTEGER NOT NULL DEFAULT 1000, + created_at TEXT NOT NULL + ) + `); + + const info = sqlite.exec("PRAGMA table_info(catalogs)"); + const colNames = new Set((info[0]?.values ?? []).map((row) => String(row[1]))); + if (!colNames.has("flip_ms")) { + sqlite.run("ALTER TABLE catalogs ADD COLUMN flip_ms INTEGER NOT NULL DEFAULT 1000"); + } + + const persist = () => { + fs.mkdirSync(path.dirname(databasePath), { recursive: true }); + fs.writeFileSync(databasePath, Buffer.from(sqlite.export())); + }; + + persist(); + + return { + sqlite, + drizzle: drizzle(sqlite, { schema: { catalogsTable } }), + persist, + }; +} diff --git a/server/env.ts b/server/env.ts new file mode 100644 index 0000000..ee35994 --- /dev/null +++ b/server/env.ts @@ -0,0 +1,30 @@ +import path from "node:path"; + +export type Env = { + port: number; + apiPort: number; + publicBaseUrl: string; + storageDir: string; + databasePath: string; + isProduction: boolean; +}; + +export function loadEnv(overrides: Partial = {}): Env { + const read = (key: string, fallback: string) => + overrides[key] ?? process.env[key] ?? fallback; + + const isProduction = + (overrides.NODE_ENV ?? process.env.NODE_ENV) === "production"; + + return { + port: Number(read("PORT", "3000")), + apiPort: Number(read("API_PORT", "3010")), + publicBaseUrl: read("PUBLIC_BASE_URL", "http://localhost:3000").replace( + /\/$/, + "", + ), + storageDir: path.resolve(read("STORAGE_DIR", "./storage")), + databasePath: path.resolve(read("DATABASE_PATH", "./data/catalogs.sqlite")), + isProduction, + }; +} diff --git a/server/errors.ts b/server/errors.ts new file mode 100644 index 0000000..14a10e9 --- /dev/null +++ b/server/errors.ts @@ -0,0 +1,19 @@ +export class HttpError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.status = status; + this.name = "HttpError"; + } +} + +export const messages = { + tooLarge: "حجم یا تعداد صفحه بیش از حد مجاز است", + badType: "نوع فایل پشتیبانی نمی‌شود", + badEffect: "افکت نامعتبر است", + notFound: "کاتالوگ پیدا نشد", + forbiddenDelete: "اجازه حذف ندارید", + rateLimited: "تعداد ساخت از این آدرس بیش از حد مجاز است", + tokenRequired: "لینک حذف نامعتبر است", +} as const; diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..341a26f --- /dev/null +++ b/server/index.ts @@ -0,0 +1,30 @@ +import { serve } from "@hono/node-server"; +import { createApp } from "./app"; +import { loadEnv } from "./env"; + +const env = loadEnv(); +const lifecycle = process.env.npm_lifecycle_event ?? ""; +const serveUi = env.isProduction || lifecycle === "start"; +const app = await createApp({ ...env, isProduction: serveUi }); +const port = serveUi ? env.port : env.apiPort; + +try { + serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, () => { + console.log( + `Listening on http://127.0.0.1:${port}${serveUi ? " (app + api)" : " (api)"}`, + ); + }); +} catch (err) { + console.error(err); + process.exit(1); +} + +process.on("uncaughtException", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + console.error( + `Port ${port} is already in use. Stop the other npm run dev / node process, then start again.`, + ); + process.exit(1); + } + throw err; +}); diff --git a/server/magic.ts b/server/magic.ts new file mode 100644 index 0000000..e39435f --- /dev/null +++ b/server/magic.ts @@ -0,0 +1,19 @@ +/** JPEG SOI: FF D8 FF */ +export function isJpeg(buffer: Buffer): boolean { + return ( + buffer.length >= 3 && + buffer[0] === 0xff && + buffer[1] === 0xd8 && + buffer[2] === 0xff + ); +} + +export function isPng(buffer: Buffer): boolean { + return ( + buffer.length >= 8 && + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 + ); +} diff --git a/server/qr.ts b/server/qr.ts new file mode 100644 index 0000000..ea087d8 --- /dev/null +++ b/server/qr.ts @@ -0,0 +1,10 @@ +import QRCode from "qrcode"; + +export async function makeQrPng(text: string): Promise { + return QRCode.toBuffer(text, { + type: "png", + width: 512, + margin: 1, + errorCorrectionLevel: "M", + }); +} diff --git a/server/rate-limit.ts b/server/rate-limit.ts new file mode 100644 index 0000000..92b53f6 --- /dev/null +++ b/server/rate-limit.ts @@ -0,0 +1,25 @@ +export type RateLimiter = { + check: (ip: string) => boolean; +}; + +export function createRateLimiter( + limit: number, + windowMs: number, + now: () => number = () => Date.now(), +): RateLimiter { + const hits = new Map(); + + return { + check(ip: string) { + const t = now(); + const recent = (hits.get(ip) ?? []).filter((ts) => t - ts < windowMs); + if (recent.length >= limit) { + hits.set(ip, recent); + return false; + } + recent.push(t); + hits.set(ip, recent); + return true; + }, + }; +} diff --git a/server/routes/catalogs.ts b/server/routes/catalogs.ts new file mode 100644 index 0000000..8043c22 --- /dev/null +++ b/server/routes/catalogs.ts @@ -0,0 +1,198 @@ +import { Hono } from "hono"; +import { nanoid } from "nanoid"; +import { + DEFAULT_TITLE, + clampFlipMs, + fileTooLargeMessage, + isEffectId, + LIMITS, + tooManyPagesMessage, + type CreateCatalogResponse, + type CatalogPublic, +} from "../../shared/types"; +import type { CatalogRepo } from "../catalog-repo"; +import type { Env } from "../env"; +import { HttpError, messages } from "../errors"; +import { isJpeg } from "../magic"; +import { makeQrPng } from "../qr"; +import type { RateLimiter } from "../rate-limit"; +import { + deleteCatalogFiles, + readPage, + readQr, + saveCatalogPages, + saveQr, + StorageError, +} from "../storage"; + +function clientIp(c: { req: { header: (name: string) => string | undefined } }): string { + return ( + c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || + c.req.header("x-real-ip") || + "127.0.0.1" + ); +} + +function toPublic(row: { + id: string; + title: string; + effect: CatalogPublic["effect"]; + pageCount: number; + flipMs: number; + createdAt: string; +}): CatalogPublic { + return { + id: row.id, + title: row.title, + effect: row.effect, + pageCount: row.pageCount, + flipMs: row.flipMs, + createdAt: row.createdAt, + }; +} + +async function readPageBuffers(pages: Blob[]): Promise { + const buffers: Buffer[] = []; + for (const file of pages) { + if (file.size > LIMITS.maxFileBytes) { + throw new HttpError(400, fileTooLargeMessage(file.size)); + } + const buf = Buffer.from(await file.arrayBuffer()); + if (!isJpeg(buf)) { + throw new HttpError(400, messages.badType); + } + buffers.push(buf); + } + return buffers; +} + +function isBlobLike(value: unknown): value is Blob { + return ( + typeof value === "object" && + value !== null && + typeof (value as Blob).arrayBuffer === "function" && + typeof (value as Blob).size === "number" + ); +} + +function asFileArray(value: unknown): Blob[] { + if (!value) return []; + const list = Array.isArray(value) ? value : [value]; + return list.filter(isBlobLike); +} + +export function catalogRoutes(opts: { + env: Env; + repo: CatalogRepo; + limiter: RateLimiter; +}) { + const app = new Hono(); + + app.post("/catalogs", async (c) => { + const ip = clientIp(c); + if (!opts.limiter.check(ip)) { + throw new HttpError(429, messages.rateLimited); + } + + const body = await c.req.parseBody({ all: true }); + const titleRaw = typeof body.title === "string" ? body.title.trim() : ""; + const title = titleRaw || DEFAULT_TITLE; + const effectRaw = typeof body.effect === "string" ? body.effect : ""; + if (!isEffectId(effectRaw)) { + throw new HttpError(400, messages.badEffect); + } + const flipMs = clampFlipMs(body.flipMs); + + const pages = asFileArray(body.pages); + if (pages.length < 1) { + throw new HttpError(400, "هیچ صفحه‌ای دریافت نشد."); + } + if (pages.length > LIMITS.maxPages) { + throw new HttpError(400, tooManyPagesMessage(pages.length)); + } + + const buffers = await readPageBuffers(pages); + const id = nanoid(10); + const deleteToken = nanoid(24); + + await saveCatalogPages(opts.env.storageDir, id, buffers); + const viewUrl = `${opts.env.publicBaseUrl}/c/${id}`; + const qrPng = await makeQrPng(viewUrl); + await saveQr(opts.env.storageDir, id, qrPng); + + opts.repo.create({ + id, + deleteToken, + title, + effect: effectRaw, + pageCount: buffers.length, + flipMs, + }); + + const payload: CreateCatalogResponse = { + id, + viewUrl, + deleteUrl: `${opts.env.publicBaseUrl}/c/${id}/delete?token=${deleteToken}`, + qrUrl: `/api/catalogs/${id}/qr`, + doneUrl: `${opts.env.publicBaseUrl}/c/${id}/done?token=${deleteToken}`, + }; + + return c.json(payload, 201); + }); + + app.get("/catalogs/:id", (c) => { + const row = opts.repo.getById(c.req.param("id")); + if (!row) throw new HttpError(404, messages.notFound); + return c.json(toPublic(row)); + }); + + app.get("/catalogs/:id/pages/:n", async (c) => { + const row = opts.repo.getById(c.req.param("id")); + if (!row) throw new HttpError(404, messages.notFound); + const n = Number(c.req.param("n")); + if (!Number.isInteger(n) || n < 1 || n > row.pageCount) { + throw new HttpError(404, messages.notFound); + } + try { + const buf = await readPage(opts.env.storageDir, row.id, n); + return new Response(buf, { + headers: { + "Content-Type": "image/jpeg", + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (err) { + if (err instanceof StorageError) throw new HttpError(404, messages.notFound); + throw err; + } + }); + + app.get("/catalogs/:id/qr", async (c) => { + const row = opts.repo.getById(c.req.param("id")); + if (!row) throw new HttpError(404, messages.notFound); + try { + const buf = await readQr(opts.env.storageDir, row.id); + return new Response(buf, { + headers: { "Content-Type": "image/png" }, + }); + } catch (err) { + if (err instanceof StorageError) throw new HttpError(404, messages.notFound); + throw err; + } + }); + + app.delete("/catalogs/:id", async (c) => { + const token = c.req.query("token") ?? ""; + if (!token) throw new HttpError(403, messages.forbiddenDelete); + const row = opts.repo.getById(c.req.param("id")); + if (!row) throw new HttpError(404, messages.notFound); + if (row.deleteToken !== token) { + throw new HttpError(403, messages.forbiddenDelete); + } + opts.repo.deleteById(row.id); + await deleteCatalogFiles(opts.env.storageDir, row.id); + return c.body(null, 204); + }); + + return app; +} diff --git a/server/schema.ts b/server/schema.ts new file mode 100644 index 0000000..e6b877f --- /dev/null +++ b/server/schema.ts @@ -0,0 +1,11 @@ +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; + +export const catalogsTable = sqliteTable("catalogs", { + id: text("id").primaryKey(), + deleteToken: text("delete_token").notNull(), + title: text("title").notNull(), + effect: text("effect").notNull(), + pageCount: integer("page_count").notNull(), + flipMs: integer("flip_ms").notNull(), + createdAt: text("created_at").notNull(), +}); diff --git a/server/storage.ts b/server/storage.ts new file mode 100644 index 0000000..83cb125 --- /dev/null +++ b/server/storage.ts @@ -0,0 +1,75 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { isJpeg } from "./magic"; + +export class StorageError extends Error { + constructor(message: string) { + super(message); + this.name = "StorageError"; + } +} + +export function pageFileName(n: number): string { + return `page-${String(n).padStart(3, "0")}.jpg`; +} + +export function catalogDir(storageDir: string, id: string): string { + return path.join(storageDir, "catalogs", id); +} + +export async function saveCatalogPages( + storageDir: string, + id: string, + pages: Buffer[], +): Promise { + const dir = catalogDir(storageDir, id); + await fs.mkdir(dir, { recursive: true }); + let i = 1; + for (const page of pages) { + if (!isJpeg(page)) { + await fs.rm(dir, { recursive: true, force: true }); + throw new StorageError("نوع فایل پشتیبانی نمی‌شود"); + } + await fs.writeFile(path.join(dir, pageFileName(i)), page); + i += 1; + } +} + +export async function saveQr( + storageDir: string, + id: string, + png: Buffer, +): Promise { + const dir = catalogDir(storageDir, id); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(path.join(dir, "qr.png"), png); +} + +export async function readPage( + storageDir: string, + id: string, + n: number, +): Promise { + const file = path.join(catalogDir(storageDir, id), pageFileName(n)); + try { + return await fs.readFile(file); + } catch { + throw new StorageError("کاتالوگ پیدا نشد"); + } +} + +export async function readQr(storageDir: string, id: string): Promise { + const file = path.join(catalogDir(storageDir, id), "qr.png"); + try { + return await fs.readFile(file); + } catch { + throw new StorageError("کاتالوگ پیدا نشد"); + } +} + +export async function deleteCatalogFiles( + storageDir: string, + id: string, +): Promise { + await fs.rm(catalogDir(storageDir, id), { recursive: true, force: true }); +} diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..5ed7bfe --- /dev/null +++ b/shared/types.ts @@ -0,0 +1,53 @@ +export const EFFECTS = ["magazine", "slide", "fade", "card", "scroll"] as const; +export type EffectId = (typeof EFFECTS)[number]; + +export function isEffectId(value: string): value is EffectId { + return (EFFECTS as readonly string[]).includes(value); +} + +export type CatalogPublic = { + id: string; + title: string; + effect: EffectId; + pageCount: number; + flipMs: number; + createdAt: string; +}; + +export type CreateCatalogResponse = { + id: string; + viewUrl: string; + deleteUrl: string; + qrUrl: string; + doneUrl: string; +}; + +export const LIMITS = { + maxFileBytes: 100 * 1024 * 1024, + maxPages: 200, + maxCreatesPerHour: 10, + maxPageWidth: 1600, + jpegQuality: 85, + rateWindowMs: 60 * 60 * 1000, + minFlipMs: 400, + maxFlipMs: 5000, +} as const; + +export const DEFAULT_TITLE = "کاتالوگ"; +export const DEFAULT_FLIP_MS = 1000; + +export function clampFlipMs(value: unknown): number { + const n = Number(value); + if (!Number.isFinite(n)) return DEFAULT_FLIP_MS; + return Math.min(LIMITS.maxFlipMs, Math.max(LIMITS.minFlipMs, Math.round(n))); +} + +export function fileTooLargeMessage(sizeBytes: number): string { + const used = (sizeBytes / (1024 * 1024)).toFixed(1); + const max = LIMITS.maxFileBytes / (1024 * 1024); + return `حجم فایل ${used} مگابایت است؛ حداکثر ${max} مگابایت مجاز است.`; +} + +export function tooManyPagesMessage(count: number): string { + return `این فایل ${count} صفحه دارد؛ حداکثر ${LIMITS.maxPages} صفحه مجاز است.`; +} diff --git a/tests/server/catalog-repo.test.ts b/tests/server/catalog-repo.test.ts new file mode 100644 index 0000000..099bbcb --- /dev/null +++ b/tests/server/catalog-repo.test.ts @@ -0,0 +1,49 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { openDatabase } from "../../server/db"; +import { createCatalogRepo } from "../../server/catalog-repo"; + +const temps: string[] = []; + +async function tempDb() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "dc-db-")); + temps.push(dir); + const db = await openDatabase(path.join(dir, "catalogs.sqlite")); + return createCatalogRepo(db); +} + +afterEach(async () => { + await Promise.all(temps.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("catalog repo", () => { + it("inserts and reads a catalog", async () => { + const repo = await tempDb(); + const created = repo.create({ + id: "abc12xyz90", + deleteToken: "token-token-token-token-12", + title: "کاتالوگ", + effect: "magazine", + pageCount: 2, + }); + const found = repo.getById("abc12xyz90"); + expect(found).toEqual(created); + expect(found?.flipMs).toBe(1000); + expect(found?.deleteToken).toBe("token-token-token-token-12"); + }); + + it("returns null after delete", async () => { + const repo = await tempDb(); + repo.create({ + id: "id1", + deleteToken: "tok", + title: "کاتالوگ", + effect: "slide", + pageCount: 1, + }); + expect(repo.deleteById("id1")).toBe(true); + expect(repo.getById("id1")).toBeNull(); + }); +}); diff --git a/tests/server/catalogs.test.ts b/tests/server/catalogs.test.ts new file mode 100644 index 0000000..2eaeafc --- /dev/null +++ b/tests/server/catalogs.test.ts @@ -0,0 +1,225 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { createApp } from "../../server/app"; +import { loadEnv } from "../../server/env"; +import { LIMITS } from "../../shared/types"; +import { jpegFile, TINY_PNG, TINY_JPEG } from "./helpers"; + +const temps: string[] = []; + +async function testApp(now?: () => number) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "dc-app-")); + temps.push(dir); + const env = loadEnv({ + PUBLIC_BASE_URL: "http://localhost:3000", + STORAGE_DIR: path.join(dir, "storage"), + DATABASE_PATH: path.join(dir, "data", "catalogs.sqlite"), + NODE_ENV: "test", + }); + return createApp(env, { now }); +} + +function form(pages: File[], extra: Record = {}) { + const data = new FormData(); + data.set("title", extra.title ?? ""); + data.set("effect", extra.effect ?? "magazine"); + if (extra.flipMs) data.set("flipMs", extra.flipMs); + for (const page of pages) data.append("pages", page); + return data; +} + +afterEach(async () => { + await Promise.all(temps.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("catalog HTTP API", () => { + it("creates a catalog and hides delete token on public GET", async () => { + const app = await testApp(); + const created = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile("a.jpg"), jpegFile("b.jpg")], { title: "نمونه" }), + headers: { "x-forwarded-for": "10.0.0.1" }, + }); + expect(created.status).toBe(201); + const body = await created.json(); + expect(body.id).toMatch(/^[A-Za-z0-9_-]{10}$/); + expect(body.viewUrl).toBe(`http://localhost:3000/c/${body.id}`); + expect(body.deleteUrl).toContain("token="); + expect(body.doneUrl).toContain("/done?token="); + expect(body.qrUrl).toBe(`/api/catalogs/${body.id}/qr`); + + const pub = await app.request(`/api/catalogs/${body.id}`); + expect(pub.status).toBe(200); + const publicBody = await pub.json(); + expect(publicBody.title).toBe("نمونه"); + expect(publicBody.pageCount).toBe(2); + expect(publicBody.flipMs).toBe(1000); + expect(publicBody.deleteToken).toBeUndefined(); + expect(JSON.stringify(publicBody)).not.toContain("delete"); + }); + + it("stores a custom flip duration", async () => { + const app = await testApp(); + const created = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()], { flipMs: "1800" }), + headers: { "x-forwarded-for": "10.0.0.12" }, + }); + expect(created.status).toBe(201); + const { id } = await created.json(); + const pub = await app.request(`/api/catalogs/${id}`); + expect((await pub.json()).flipMs).toBe(1800); + }); + + it("clamps flip duration to the allowed range", async () => { + const app = await testApp(); + const tooFast = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()], { flipMs: "50" }), + headers: { "x-forwarded-for": "10.0.0.13" }, + }); + const tooSlow = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()], { flipMs: "9000" }), + headers: { "x-forwarded-for": "10.0.0.14" }, + }); + const fastId = (await tooFast.json()).id; + const slowId = (await tooSlow.json()).id; + expect((await (await app.request(`/api/catalogs/${fastId}`)).json()).flipMs).toBe( + LIMITS.minFlipMs, + ); + expect((await (await app.request(`/api/catalogs/${slowId}`)).json()).flipMs).toBe( + LIMITS.maxFlipMs, + ); + }); + + it("serves jpeg pages and png qr", async () => { + const app = await testApp(); + const created = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()]), + headers: { "x-forwarded-for": "10.0.0.2" }, + }); + const { id } = await created.json(); + + const page = await app.request(`/api/catalogs/${id}/pages/1`); + expect(page.status).toBe(200); + expect(page.headers.get("content-type")).toBe("image/jpeg"); + + const qr = await app.request(`/api/catalogs/${id}/qr`); + expect(qr.status).toBe(200); + expect(qr.headers.get("content-type")).toBe("image/png"); + const qrBytes = Buffer.from(await qr.arrayBuffer()); + expect(qrBytes[0]).toBe(0x89); + expect(qrBytes[1]).toBe(0x50); + }); + + it("rejects delete without token and keeps the catalog", async () => { + const app = await testApp(); + const created = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()]), + headers: { "x-forwarded-for": "10.0.0.3" }, + }); + const { id } = await created.json(); + const denied = await app.request(`/api/catalogs/${id}`, { method: "DELETE" }); + expect(denied.status).toBe(403); + expect((await app.request(`/api/catalogs/${id}`)).status).toBe(200); + }); + + it("deletes with token then returns 404", async () => { + const app = await testApp(); + const created = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()]), + headers: { "x-forwarded-for": "10.0.0.4" }, + }); + const body = await created.json(); + const token = new URL(body.deleteUrl).searchParams.get("token"); + const del = await app.request(`/api/catalogs/${body.id}?token=${token}`, { + method: "DELETE", + }); + expect(del.status).toBe(204); + expect((await app.request(`/api/catalogs/${body.id}`)).status).toBe(404); + }); + + it("rejects more pages than the limit", async () => { + const app = await testApp(); + const pages = Array.from({ length: LIMITS.maxPages + 1 }, (_, i) => + jpegFile(`${i}.jpg`), + ); + const res = await app.request("/api/catalogs", { + method: "POST", + body: form(pages), + headers: { "x-forwarded-for": "10.0.0.5" }, + }); + expect(res.status).toBe(400); + expect((await res.json()).error).toContain("صفحه"); + }); + + it("accepts a jpeg Blob the way the browser sends pages", async () => { + const app = await testApp(); + const data = new FormData(); + data.set("title", ""); + data.set("effect", "slide"); + data.append( + "pages", + new Blob([TINY_JPEG], { type: "image/jpeg" }), + "page-001.jpg", + ); + const res = await app.request("/api/catalogs", { + method: "POST", + body: data, + headers: { "x-forwarded-for": "10.0.0.11" }, + }); + expect(res.status).toBe(201); + }); + + it("rejects an invalid effect", async () => { + const app = await testApp(); + const res = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()], { effect: "explode" }), + headers: { "x-forwarded-for": "10.0.0.6" }, + }); + expect(res.status).toBe(400); + expect((await res.json()).error).toBe("افکت نامعتبر است"); + }); + + it("rejects non-jpeg pages", async () => { + const app = await testApp(); + const res = await app.request("/api/catalogs", { + method: "POST", + body: form([new File([TINY_PNG], "x.png", { type: "image/png" })]), + headers: { "x-forwarded-for": "10.0.0.7" }, + }); + expect(res.status).toBe(400); + expect((await res.json()).error).toBe("نوع فایل پشتیبانی نمی‌شود"); + }); + + it("rate-limits the 11th create from one IP", async () => { + const app = await testApp(() => 1_000); + for (let i = 0; i < 10; i++) { + const res = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()]), + headers: { "x-forwarded-for": "10.0.0.8" }, + }); + expect(res.status).toBe(201); + } + const blocked = await app.request("/api/catalogs", { + method: "POST", + body: form([jpegFile()]), + headers: { "x-forwarded-for": "10.0.0.8" }, + }); + expect(blocked.status).toBe(429); + }); + + it("has no public catalog list", async () => { + const app = await testApp(); + const res = await app.request("/api/catalogs"); + expect(res.status).toBe(404); + }); +}); diff --git a/tests/server/helpers.ts b/tests/server/helpers.ts new file mode 100644 index 0000000..a17cbef --- /dev/null +++ b/tests/server/helpers.ts @@ -0,0 +1,10 @@ +/** Minimal JPEG (SOI + EOI) used only for API/storage tests. */ +export const TINY_JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xd9]); + +export const TINY_PNG = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, +]); + +export function jpegFile(name = "page.jpg"): File { + return new File([TINY_JPEG], name, { type: "image/jpeg" }); +} diff --git a/tests/server/rate-limit.test.ts b/tests/server/rate-limit.test.ts new file mode 100644 index 0000000..720d731 --- /dev/null +++ b/tests/server/rate-limit.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { createRateLimiter } from "../../server/rate-limit"; + +describe("createRateLimiter", () => { + it("allows the first 10 hits from an IP", () => { + let now = 1_000; + const limiter = createRateLimiter(10, 60 * 60 * 1000, () => now); + + for (let i = 0; i < 10; i++) { + expect(limiter.check("1.1.1.1")).toBe(true); + } + }); + + it("rejects the 11th hit within the window", () => { + let now = 1_000; + const limiter = createRateLimiter(10, 60 * 60 * 1000, () => now); + + for (let i = 0; i < 10; i++) { + limiter.check("1.1.1.1"); + } + + expect(limiter.check("1.1.1.1")).toBe(false); + }); + + it("allows another hit after the window elapses", () => { + let now = 1_000; + const windowMs = 60 * 60 * 1000; + const limiter = createRateLimiter(10, windowMs, () => now); + + for (let i = 0; i < 10; i++) { + limiter.check("1.1.1.1"); + } + + now += windowMs + 1; + expect(limiter.check("1.1.1.1")).toBe(true); + }); + + it("tracks IPs independently", () => { + const limiter = createRateLimiter(1, 60_000, () => 1_000); + expect(limiter.check("a")).toBe(true); + expect(limiter.check("a")).toBe(false); + expect(limiter.check("b")).toBe(true); + }); +}); diff --git a/tests/server/storage.test.ts b/tests/server/storage.test.ts new file mode 100644 index 0000000..ed49848 --- /dev/null +++ b/tests/server/storage.test.ts @@ -0,0 +1,48 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + deleteCatalogFiles, + readPage, + saveCatalogPages, + StorageError, +} from "../../server/storage"; +import { TINY_JPEG, TINY_PNG } from "./helpers"; + +const temps: string[] = []; + +async function tempDir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "dc-storage-")); + temps.push(dir); + return dir; +} + +afterEach(async () => { + await Promise.all(temps.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("saveCatalogPages", () => { + it("writes numbered jpegs and can read them back", async () => { + const dir = await tempDir(); + await saveCatalogPages(dir, "abc", [TINY_JPEG, TINY_JPEG]); + const page1 = await readPage(dir, "abc", 1); + expect(page1.equals(TINY_JPEG)).toBe(true); + const page2 = await readPage(dir, "abc", 2); + expect(page2.equals(TINY_JPEG)).toBe(true); + }); + + it("rejects non-jpeg pages", async () => { + const dir = await tempDir(); + await expect(saveCatalogPages(dir, "abc", [TINY_PNG])).rejects.toBeInstanceOf( + StorageError, + ); + }); + + it("deletes files so later reads fail", async () => { + const dir = await tempDir(); + await saveCatalogPages(dir, "abc", [TINY_JPEG]); + await deleteCatalogFiles(dir, "abc"); + await expect(readPage(dir, "abc", 1)).rejects.toBeInstanceOf(StorageError); + }); +}); diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..fab5831 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./client/src/*"], + "@shared/*": ["./shared/*"] + } + }, + "include": ["client/src/**/*.ts", "client/src/**/*.vue", "shared/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..cf0c15b --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "server/**/*.ts", + "shared/**/*.ts", + "tests/**/*.ts" + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f180dc8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath, URL } from "node:url"; +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + root: "client", + plugins: [vue()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./client/src", import.meta.url)), + "@shared": fileURLToPath(new URL("./shared", import.meta.url)), + "page-flip": fileURLToPath( + new URL("./node_modules/page-flip/dist/js/page-flip.module.js", import.meta.url), + ), + }, + }, + server: { + host: "127.0.0.1", + port: 3000, + strictPort: true, + proxy: { + "/api": "http://127.0.0.1:3010", + }, + }, + build: { + outDir: "../dist", + emptyOutDir: true, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..eefa52e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + environment: "node", + testTimeout: 20000, + fileParallelism: false, + }, +});