diff --git a/.claude/settings.json b/.claude/settings.json index ff62fee..c49106c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -73,7 +73,24 @@ "Bash(python assets/scripts/search.py \"steel industry admin dashboard CMS analytics RTL\" --design-system -p \"Foolad Panel\")", "Bash(Get-ChildItem -Path \"c:\\\\Users\\\\DATIS STAR\\\\پنل\\\\src\\\\app\\\\api\\\\\" -Recurse -File)", "Read(//c/Users/DATIS STAR/**)", +<<<<<<< HEAD "Bash(git branch *)" +======= + "Bash(git branch *)", + "PowerShell(\\(Get-Item \"c:\\\\Users\\\\DATIS STAR\\\\andishkade-foolad\\\\public\\\\logo.png\"\\).Length)", + "Bash(Get-ChildItem -Path \"c:\\\\Users\\\\DATIS STAR\\\\andishkade-foolad\" -Force)", + "Bash(Select-Object Name, Mode)", + "Bash(pip --version)", + "Bash(pip install *)", + "Bash(python -m hf_xet install-filter)", + "Bash(hf_xet version *)", + "Bash(hf_xet --help)", + "Read(//c/Python314/Scripts/**)", + "Read(//c/Python314/Lib/site-packages/hf_xet/**)", + "Bash(huggingface-cli version *)", + "Bash(hf auth *)", + "Bash(hf upload *)" +>>>>>>> space/main ], "additionalDirectories": [ "c:\\Users\\DATIS STAR\\پنل" diff --git a/.gitignore b/.gitignore index 8fdc2cc..88963d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +<<<<<<< HEAD # Logs logs *.log @@ -27,3 +28,34 @@ dist-ssr /[0-9]*.png brand_pages/ /موکاپ*.jpg +======= +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# Root-level temp images (not website assets) +/[0-9]*.jpg +/[0-9]*.png +brand_pages/ +/موکاپ*.jpg +>>>>>>> space/main diff --git a/.playwright-mcp/page-2026-06-09T07-11-59-434Z.yml b/.playwright-mcp/page-2026-06-09T07-11-59-434Z.yml new file mode 100644 index 0000000..3f59b3d --- /dev/null +++ b/.playwright-mcp/page-2026-06-09T07-11-59-434Z.yml @@ -0,0 +1,249 @@ +- generic [active] [ref=e1]: + - generic: + - img "Spread" + - generic: "100" + - navigation [ref=e2]: + - img "Spread" [ref=e4] + - list [ref=e5]: + - listitem [ref=e6]: + - link "How it works" [ref=e7] [cursor=pointer]: + - /url: "#how" + - listitem [ref=e8]: + - link "See a deal" [ref=e9] [cursor=pointer]: + - /url: "#deal" + - listitem [ref=e10]: + - link "Niches" [ref=e11] [cursor=pointer]: + - /url: "#niches" + - listitem [ref=e12]: + - link "Pricing" [ref=e13] [cursor=pointer]: + - /url: "#pricing" + - link "Get early access" [ref=e14] [cursor=pointer]: + - /url: /signup + - button "Get early access" [ref=e15] + - generic [ref=e26]: + - iframe [ref=e27]: + - region "Video Player" [ref=f2e2]: + - application [ref=f2e3] + - text:   + - generic [ref=e29]: + - heading "Find what's underpriced. Sell it for what it's worth." [level=1] [ref=e30]: + - generic [ref=e32]: Find + - generic [ref=e34]: what's + - generic [ref=e36]: underpriced. + - generic [ref=e38]: Sell + - generic [ref=e40]: it + - generic [ref=e42]: for + - generic [ref=e44]: what + - generic [ref=e46]: it's + - generic [ref=e48]: worth. + - paragraph [ref=e49]: AI that scans every marketplace, scores every deal, and helps you relist in one tap. Stop scrolling. Start flipping. + - generic [ref=e50]: + - link "Join the waitlist" [ref=e51] [cursor=pointer]: + - /url: /signup + - button "Join the waitlist" [ref=e52]: + - generic: Join the waitlist + - link "See it work →" [ref=e53] [cursor=pointer]: + - /url: "#deal" + - link "↗" [ref=e54] [cursor=pointer]: + - /url: /signup + - img [ref=e55]: + - generic [ref=e57]: CAPTURE THE SPREAD • FLIP SMARTER • CAPTURE THE SPREAD • FLIP SMARTER • + - generic: ↗ + - generic [ref=e59]: + - generic [ref=e60]: How it works + - heading "The AI finds deals you'd never see in time." [level=2] [ref=e61] + - paragraph [ref=e62]: Spread scans thousands of listings across Facebook Marketplace, Craigslist, OfferUp, eBay, and more — alerting you the second something surfaces 30–60% below market value in your niches. + - generic [ref=e63]: + - strong [ref=e64]: 6 platforms. + - text: Scanned 24/7. Alerts in under 12 seconds. + - generic [ref=e67]: + - generic [ref=e68]: Score & relist + - heading "Know exactly what to offer. List it before you leave." [level=2] [ref=e69] + - paragraph [ref=e70]: Every deal gets an AI score — estimated resale value, projected margin, time-to-sell, and the best platform to relist on. One tap generates an optimized listing with AI-written descriptions. + - generic [ref=e71]: + - generic [ref=e74]: Spread AI · Deal Analysis + - generic [ref=e75]: + - generic [ref=e76]: + - text: Found a + - strong [ref=e77]: Breville Oracle Touch + - text: listed at $1,200 + - generic [ref=e78]: "Market value: $1,800–$2,100 on eBay" + - generic [ref=e79]: "Suggested offer: $750 · Projected profit: +$500" + - generic [ref=e80]: Generate opener message ↗ + - generic [ref=e81]: "\"Hi — is this still available? I can pick up today. Would you take $750 cash?\"" + - generic [ref=e82]: Opener copied · eBay listing drafted + - generic [ref=e86]: + - generic [ref=e87]: + - heading "Stop scrolling. Start flipping." [level=2] [ref=e88] + - paragraph [ref=e89]: Other flippers spend 20 hours a week refreshing marketplace feeds. You spend 20 minutes reviewing deals the AI already found. + - generic [ref=e90]: + - generic [ref=e91]: + - generic [ref=e92]: "0" + - generic [ref=e93]: Marketplaces scanned 24/7 + - generic [ref=e94]: + - generic [ref=e95]: 0% + - generic [ref=e96]: Average margin per deal + - generic [ref=e97]: + - generic [ref=e98]: 0hrs + - generic [ref=e99]: Saved per week vs. manual + - generic [ref=e100]: + - generic [ref=e101]: 0s + - generic [ref=e102]: Avg. alert speed on new listings + - generic [ref=e104]: + - generic [ref=e105]: Live opportunities + - heading "This is what the AI surfaces every day." [level=2] [ref=e106] + - paragraph [ref=e107]: Real arbitrage opportunities — scored, analyzed, and ready for you to act on. This is what your feed looks like. + - generic [ref=e112]: + - generic: “ + - generic [ref=e113]: + - text: I used to spend 3 hours a day refreshing Marketplace. Now Spread sends me the good stuff and I just act on it. Made $1,200 my first month. + - generic [ref=e114]: The deal scoring is scary accurate. It told me to offer $380 on an Aeron listed at $520. I sold it for $710 on Apt Deco two days later. + - generic [ref=e115]: One-click cross-listing changed everything. I find it on Craigslist, hit relist, and the eBay listing writes itself. My volume tripled. + - generic [ref=e116]: Set up my espresso machine niche on a Sunday. Had three scored deals by Monday morning. Closed my first flip by Wednesday. + - generic [ref=e117]: + - text: Operator plan · Austin, TX + - generic [ref=e118]: Scout plan · Portland, OR + - generic [ref=e119]: Operator plan · Nashville, TN + - generic [ref=e120]: Scout plan · Denver, CO + - generic [ref=e121]: + - button "Sarah K." [ref=e122] [cursor=pointer]: + - generic [ref=e123]: Sarah K. + - button "Marcus T." [ref=e124] [cursor=pointer]: + - generic: Marcus T. + - button "David L." [ref=e125] [cursor=pointer]: + - generic: David L. + - button "Amy R." [ref=e126] [cursor=pointer]: + - generic: Amy R. + - generic [ref=e127]: + - generic [ref=e128]: Built for flippers + - generic [ref=e130]: Every marketplace has a pricing gap. + - generic [ref=e132]: The AI finds it. You capture it. + - generic [ref=e134]: + - generic [ref=e135]: Where we're headed + - heading "Your money. Your rules. Choose how hands-on you want to be." [level=2] [ref=e136] + - paragraph [ref=e137]: Today, you control every deal. Soon, the AI handles more — negotiation, auto-buy within your thresholds, and fully autonomous operation. You decide when to let go. + - generic [ref=e138]: + - generic [ref=e139]: + - button "Manual" [ref=e140] [cursor=pointer] + - button "Assisted SOON" [ref=e141] [cursor=pointer] + - button "Autopilot SOON" [ref=e142] [cursor=pointer] + - paragraph [ref=e144]: Available now. The AI finds deals, scores them, and drafts your relist. You decide what to buy, what to offer, and when to sell. Full control, full visibility, full profit tracking. + - generic [ref=e145]: + - generic [ref=e146]: + - generic [ref=e147]: "01" + - generic [ref=e148]: Deal found + - generic [ref=e149]: AI scans 6 platforms + - generic [ref=e150]: + - generic [ref=e151]: "02" + - generic [ref=e152]: Scored + - generic [ref=e153]: Profit + risk analysis + - generic [ref=e154]: + - generic [ref=e155]: "03" + - generic [ref=e156]: You act + - generic [ref=e157]: Message, offer, buy + - generic [ref=e158]: + - generic [ref=e159]: "04" + - generic [ref=e160]: Relist + - generic [ref=e161]: One-tap cross-listing + - generic [ref=e164]: + - generic [ref=e165]: + - generic [ref=e166]: Your niches + - heading "Pick the markets you know." [level=2] [ref=e167] + - paragraph [ref=e168]: Choose the categories you understand. The AI handles scanning, scoring, and listing across every marketplace. + - generic [ref=e169]: + - button "Espresso Machines $280" [ref=e170] [cursor=pointer] + - button "Camera Gear $320" [ref=e171] [cursor=pointer] + - button "Mid-Century Furniture $450" [ref=e172] [cursor=pointer] + - button "Power Tools $175" [ref=e173] [cursor=pointer] + - button "Commercial Kitchen $520" [ref=e174] [cursor=pointer] + - button "Audiophile Gear $210" [ref=e175] [cursor=pointer] + - button "Outdoor Gear $140" [ref=e176] [cursor=pointer] + - button "Vintage Watches $380" [ref=e177] [cursor=pointer] + - button "Sneakers $95" [ref=e178] [cursor=pointer] + - button "Vinyl Records $85" [ref=e179] [cursor=pointer] + - button "Mechanical Keyboards $120" [ref=e180] [cursor=pointer] + - button "Designer Handbags $340" [ref=e181] [cursor=pointer] + - generic [ref=e183]: + - paragraph [ref=e184]: Or search your own + - generic [ref=e185]: + - textbox "Search a niche — vintage watches, vinyl records..." [ref=e186] + - button [ref=e187] [cursor=pointer]: + - img [ref=e188] + - generic [ref=e193]: + - heading "Opportunities land while you sleep." [level=2] [ref=e194] + - paragraph [ref=e195]: The AI never stops scanning. Wake up to scored deals ready for your review. + - generic [ref=e196]: + - generic [ref=e197]: + - generic [ref=e198]: + - generic [ref=e199]: Pricing + - heading "One good flip pays for a year." [level=2] [ref=e200] + - generic [ref=e201]: No speed throttling, no alert delays. Every plan gets the same fast scanning. Upgrade for more niches and advanced tools. + - generic [ref=e202]: + - generic [ref=e203]: + - generic [ref=e204]: Starter + - generic [ref=e205]: Scout + - generic [ref=e206]: Find your first flips + - generic [ref=e207]: + - generic [ref=e208]: $29 + - generic [ref=e209]: /mo + - generic [ref=e210]: Cancel anytime + - list [ref=e212]: + - listitem [ref=e213]: 2 active niches + - listitem [ref=e214]: Real-time deal alerts + - listitem [ref=e215]: AI deal scoring + - listitem [ref=e216]: Profit tracking dashboard + - link "Start free trial" [ref=e217] [cursor=pointer]: + - /url: /signup + - button "Start free trial" [ref=e218] + - generic [ref=e219]: + - generic [ref=e220]: Most popular + - generic [ref=e221]: Operator + - generic [ref=e222]: Scale your operation + - generic [ref=e223]: + - generic [ref=e224]: $49 + - generic [ref=e225]: /mo + - generic [ref=e226]: Pays for itself in one flip + - list [ref=e228]: + - listitem [ref=e229]: 8 active niches + - listitem [ref=e230]: One-click cross-listing + - listitem [ref=e231]: AI opener messages + - listitem [ref=e232]: Niche trend intelligence + - link "Start free trial" [ref=e233] [cursor=pointer]: + - /url: /signup + - button "Start free trial" [ref=e234] + - generic [ref=e235]: + - generic [ref=e236]: Pro + - generic [ref=e237]: Syndicate + - generic [ref=e238]: For full-time flippers + - generic [ref=e239]: + - generic [ref=e240]: $99 + - generic [ref=e241]: /mo + - generic [ref=e242]: Priority access to Autopilot + - list [ref=e244]: + - listitem [ref=e245]: Unlimited niches + - listitem [ref=e246]: Multi-platform analytics + - listitem [ref=e247]: Team seats + shared deals + - listitem [ref=e248]: "Early access: Assisted + Autopilot" + - link "Start free trial" [ref=e249] [cursor=pointer]: + - /url: /signup + - button "Start free trial" [ref=e250] + - generic [ref=e252]: + - heading "Built for the way you actually flip." [level=2] [ref=e253] + - paragraph [ref=e254]: Deals surface in the background. Your life stays in the foreground. + - generic [ref=e270]: + - heading "Every marketplace has a spread. Start capturing yours." [level=2] [ref=e271]: + - text: Every marketplace has a spread. + - text: Start capturing yours. + - paragraph [ref=e272]: Same item. Different platforms. Different prices. The AI finds the gap — you keep what's left. + - link "Start free trial" [ref=e273] [cursor=pointer]: + - /url: /signup + - button "Start free trial" [ref=e274]: + - generic: Start free trial + - contentinfo [ref=e275]: + - img "Spread" [ref=e277] + - generic [ref=e278]: + - link "Terms" [ref=e279] [cursor=pointer]: + - /url: /terms + - link "Privacy" [ref=e280] [cursor=pointer]: + - /url: /privacy + - generic [ref=e281]: © 2026 Spread \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..845e74b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Two independent apps in one repo: + +1. **Frontend** (repo root) — a Vite + React 19 + TypeScript marketing/content site for "اندیشکده فولاد آینده" (a steel-industry think tank). Persian-first, RTL, bilingual (fa/en). Deploys as a static build via Docker to Hugging Face Spaces on port 7860. +2. **Admin panel** (`panel/`) — a **standalone** Node/Express + SQLite app with its own `package.json` and `node_modules`. Manages content the site is meant to publish. **The frontend does NOT consume the panel yet** — every section renders hardcoded mock data. Wiring them is a deliberate future step. + +These two have **no shared build or dependency tree**. Treat `panel/` as a separate project. + +## Commands + +Frontend (run from repo root): +```bash +npm run dev # Vite dev server (HMR) +npm run build # tsc -b && vite build (type-checks then bundles) +npm run lint # eslint . +npm run preview # serve the production build +npx tsc -b --noEmit # type-check only — the fast inner loop; use after every edit +``` +There is **no test framework** configured. "Verifying" a change means `npx tsc -b --noEmit` (must be clean) and looking at the running dev server. + +Admin panel (run from `panel/`): +```bash +cd panel +npm install # first time only +npm run seed # create the admin user from .env (ADMIN_USERNAME/PASSWORD) +npm start # node server.js → http://localhost:3001 +npm run dev # node --watch server.js +npm rebuild better-sqlite3 # REQUIRED if Node's ABI changed — native addon, fails to load otherwise +``` + +## Critical build gotcha + +`panel/` lives inside the frontend's project root, so **Vite's dependency scanner reads `panel/package.json`**. If that file (or anything Vite scans) has unresolved git merge-conflict markers (`<<<<<<<`), `npm run dev`/`build` fails with a cryptic `JSONError ... expected value at line 1`. Keep `panel/package.json` valid JSON. + +## Frontend architecture + +- **Routing**: `src/app/router.tsx` (`createBrowserRouter`). `RootLayout` (`src/app/layout/RootLayout.tsx`) wraps every page with `Header`, `Footer`, and the smooth-scroll engine. Pages live in `src/pages//.tsx`; the homepage composes section components from `src/pages/Home/sections/`. +- **Language**: `useLang()` from `src/context/LangContext` returns `{ lang: 'fa'|'en', toggle }`. Components hold inline `{ fa: {...}, en: {...} }` objects and index by `lang`. RTL is driven by `dir={lang==='fa'?'rtl':'ltr'}` near the page root; many flex containers force `direction:'ltr'` locally to stop RTL from reversing their child order, then set inner text back to `rtl`. +- **Smooth scroll**: `RootLayout` runs **Lenis synced to the GSAP ticker** (the joinspread.app recipe) — one shared RAF clock so scroll-driven animations don't stutter. It is **desktop-only**: on touch/coarse pointers it hands off to native momentum (avoids the iOS double-smoothing lag). Do not re-add CSS `scroll-behavior: smooth` — it fights Lenis. +- **Hero** (`src/pages/Home/sections/HeroSection.tsx`): a GSAP `ScrollTrigger` scrub timeline pinned over a `200vh` section (de-zoom + satellite scatter). This is GSAP, not framer-motion. +- **Styling**: mostly **inline `style` objects**, with Tailwind utility classes for responsive overrides (`max-md:`, `max-lg:`) and a few `!important` overrides. Global CSS variables in `src/index.css` (`--ink`, `--paper`, `--red`, `--color-orange`, etc.). Brand colors are **gold `#CD9E53`** and **navy `#032340`** — these are also hardcoded as `const GOLD`/`const NAVY` in many section files, so a rebrand is a repo-wide find-and-replace, not just a CSS-var change. +- **Globe** (`src/components/ui/globe.tsx`): three-globe mounted in react-three-fiber via `` (NOT via `extend`/JSX-element augmentation — that augmentation poisons global JSX types and breaks unrelated components like `React.ElementType` icons). Country polygons load at runtime from `public/globe-countries.geojson` (bundled, public-domain Natural Earth). +- **Calendar** (`src/components/ui/EventCalendar.tsx`): a native Jalali (Persian) month grid built with `Intl.DateTimeFormat('en-US-u-ca-persian', …)` — **no date library**. It walks Gregorian `Date`s and reads their Persian parts to lay out the grid. +- **Content layer**: `src/content/*.ts` and inline arrays hold all section data. These are mock/seed data and are explicitly intended to be replaced by `fetch` calls to the panel API later (see "Wiring", below). Keep new editorial data in this shape so the swap stays mechanical. + +## Admin panel architecture (`panel/`) + +- **Stack**: Express 4 + `better-sqlite3` (single file `panel/data.db`), `bcryptjs` + `jsonwebtoken` (HTTP-only cookie named `session`), `multer` uploads to `panel/uploads/`. Admin UI is a **vanilla-JS SPA** in `panel/public/` (`app.js`, no build step). +- **Schema**: `panel/db.js` (`users`, `articles`, `risk_signals`, `prices`) plus row→object mappers. `articles` is a flexible universal content model discriminated by `category`/`type`. +- **API** (`panel/server.js`): public reads (`GET /api/articles`, `/api/risks`, `/api/prices`) + auth-gated writes via the `authRequired` middleware. CRUD for articles, risk signals, and users; `/api/uploads`; `/api/contact`. CORS is gated by the `ALLOWED_ORIGINS` env var. +- **Prices** are scraped from tgju.org by `panel/scraper.js` on a 120s interval when the server runs. +- **Config**: `panel/.env` (copy from `.env.example`) — `JWT_SECRET`, `ADMIN_*`, `PORT` (3001), `ALLOWED_ORIGINS`, mail creds. + +## Wiring the panel to the site (not done yet) + +When asked to make published content appear on the site: add a frontend fetch client (e.g. `src/lib/api.ts` reading `import.meta.env.VITE_API_URL`), replace a section's `src/content/*` import with a hook that calls `GET /api/articles?category=
`, and keep the static file as the loading fallback. Do it one section at a time. The `articles` table's `category` column is the per-section discriminator. Recommended schema additions before going live: a `status` (draft/published) column, and an `events` resource for the calendar. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..904271f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,290 @@ +# SECURITY.md — Security Rules for This Project + +> **To the AI agent reading this:** These are **mandatory, non-negotiable rules**. Follow them on every change you make, even when the user doesn't mention security. When a requested feature conflicts with a rule here, implement the secure version and tell the user what you did and why. If you are unsure whether something is safe, **stop and ask** rather than guessing. Never disable a security control to "make it work" — fix the underlying cause instead. + +This file exists because apps built fast (including AI-assisted ones) repeatedly ship the same handful of vulnerabilities. The goal is an app that is "immune" to the common classes of attack by **default**, not as an afterthought. + +--- + +## 0. The Golden Rules (read these first) + +1. **The frontend is public.** Anything in client-side code, network responses, or the browser is visible to and editable by an attacker. Never trust it. All real security must live on the server / database. +2. **Never trust input.** Validate, sanitize, and authorize *every* input on the server — query params, headers, body, file uploads, cookies, and webhook payloads. +3. **Authenticate, then authorize, on every request.** Logging in is not enough. Check *this specific user is allowed to do this specific action on this specific resource* — server-side, every time. +4. **Secrets stay on the server.** No keys, tokens, passwords, or connection strings in frontend code, repos, or logs. Ever. +5. **Fail closed.** When something is ambiguous or errors out, deny access. Default to the most restrictive option. +6. **Least privilege everywhere.** Every key, role, token, and DB account gets the minimum permissions it needs and nothing more. + +--- + +## 1. Secrets & Credentials + +**NEVER** +- Hardcode API keys, passwords, JWT secrets, DB URLs, or private tokens anywhere in source. +- Put secrets in frontend/client code or in any `NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, or otherwise client-exposed env var. +- Commit `.env`, `.env.local`, credential files, `*.pem`, or service-account JSON to git. +- Log secrets, tokens, or full request bodies that may contain them. +- Reuse the same secret across dev/staging/prod. + +**ALWAYS** +- Load secrets from server-side environment variables only. +- Add `.env*`, `*.pem`, `*.key`, `secrets/`, and credential files to `.gitignore` *before* the first commit. +- Provide a `.env.example` with key **names only** and dummy values. +- Use a distinct, high-entropy (32+ random bytes) value for each signing/encryption secret. +- Assume any secret that ever touched the frontend or a public repo is **compromised** — rotate it. +- Use a secrets manager (or the platform's encrypted env store) in production, not files on disk. + +--- + +## 2. Authentication + +**NEVER** +- Roll your own crypto, password hashing, or session logic when a vetted library/provider exists. +- Store passwords in plaintext or with fast/weak hashes (MD5, SHA1, unsalted SHA256). +- Trust a `userId`, `role`, `isAdmin`, or `email` value that came from the client to decide who someone is. +- Put auth state only in `localStorage` and treat its presence as proof of identity. +- Leave default/seed credentials (`admin/admin`) in any environment. +- Return "user not found" vs "wrong password" differently (enables user enumeration). + +**ALWAYS** +- Use a maintained auth library/provider (e.g. the platform's auth, Auth.js, Lucia, Clerk, Supabase Auth) and follow its server-side session verification. +- Hash passwords with bcrypt, scrypt, or Argon2 with a proper cost factor. +- Verify the session/token **on the server** for every protected request and derive identity from it — never from request body params. +- Enforce a password policy + check against known-breached passwords where possible. +- Implement account lockout / throttling on repeated failed logins. +- Offer/encourage MFA for sensitive accounts. +- Expire sessions, rotate on privilege change, and invalidate on logout server-side. + +--- + +## 3. Authorization (the #1 vibe-coded flaw) + +> Most rapidly-built apps authenticate but barely authorize. This is where IDOR, broken access control, and data leaks come from. + +**NEVER** +- Hide a button/route on the frontend and call it "protected." UI hiding is not access control. +- Use sequential/guessable IDs as the *only* thing protecting a resource (`/api/orders/1043` → an attacker just tries `1044`). This is **IDOR**. +- Assume that because the UI only sends valid requests, the server only receives valid requests. +- Let one user's token read/modify another user's data because the query didn't filter by owner. + +**ALWAYS** +- On every data access, check ownership/permission server-side: `WHERE owner_id = :current_user` (or equivalent), not just `WHERE id = :id`. +- Re-check role/permission for every privileged action on the server, on every call. +- Enforce authorization at the **lowest layer possible** (DB row-level security + API layer), so a missed check in one place doesn't expose everything. +- Use unguessable IDs (UUIDs/ULIDs) for resources, *and still* authorize them. +- Centralize authorization logic so it's consistent and auditable, not copy-pasted per route. + +--- + +## 4. Database Security (Supabase / Firebase / direct DB) + +> Vibe-coded apps frequently expose the DB straight to the browser with permissive rules. This is catastrophic. + +**NEVER** +- Ship Supabase/Postgres tables with **Row Level Security (RLS) disabled** on anything holding user data. +- Use Firebase/Firestore rules like `allow read, write: if true;` or test-mode rules in production. +- Expose a service-role / admin DB key to the client (the anon/public key is the only client-side key, and even that needs RLS behind it). +- Build SQL by string concatenation or template interpolation with user input. +- Run app queries as a superuser/admin DB account. + +**ALWAYS** +- Enable RLS on every table and write explicit policies scoped to the authenticated user. +- Write Firestore/RTDB rules that check `request.auth` and validate ownership and shape. +- Use parameterized queries / prepared statements / a query builder / ORM bindings — **always** (prevents SQL injection). +- Give the app a least-privilege DB role (no DROP/ALTER, only needed tables). +- Validate data shape and types at the DB boundary, not just the UI. +- Keep the service-role key strictly server-side for trusted admin operations. + +--- + +## 5. Input Validation & Injection + +**NEVER** +- Pass user input directly into SQL, shell commands, `eval`, template engines, `dangerouslySetInnerHTML`, file paths, or `Function()`. +- Rely on frontend validation as a security boundary (it's UX only). +- Build file paths from user input without normalizing (enables **path traversal**, `../../etc/passwd`). + +**ALWAYS** +- Validate every input server-side against a strict schema (e.g. Zod, Pydantic, Joi) — type, length, format, allowed values. +- Allow-list rather than block-list whenever possible. +- Use parameterized queries for SQL and safe APIs for OS/command operations (avoid shelling out with user input at all). +- Reject unexpected fields to prevent **mass assignment** (don't blindly spread `req.body` into a DB record — pick allowed fields explicitly). +- Canonicalize and confine file paths to an intended directory. + +--- + +## 6. Output / XSS / Content + +**NEVER** +- Render untrusted content as raw HTML (`dangerouslySetInnerHTML`, `innerHTML`, `v-html`, `{{{ }}}`) without sanitizing. +- Reflect user input into responses unescaped. +- Inject user-controlled values into ` + + + diff --git a/live_prices.js b/live_prices.js new file mode 100644 index 0000000..11776ef --- /dev/null +++ b/live_prices.js @@ -0,0 +1,50 @@ +window.LIVE_COIN_PRICES = { + "last_fetch": "2026-06-01 16:15:58", + "prices": [ + { + "name": "سکه امامی", + "price_rial": 1845050000, + "price_toman": 184505000, + "change": "(3.07%) 55,000,000", + "min_toman": 182480000, + "max_toman": 184510000, + "last_update": "16:13:54" + }, + { + "name": "سکه بهار آزادی", + "price_rial": 1810100000, + "price_toman": 181010000, + "change": "(2.26%) 39,950,000", + "min_toman": 177980000, + "max_toman": 181020000, + "last_update": "16:14:00" + }, + { + "name": "نیم سکه", + "price_rial": 940000000, + "price_toman": 94000000, + "change": "(2.1%) 19,300,000", + "min_toman": 93500000, + "max_toman": 94000000, + "last_update": "11:10:35" + }, + { + "name": "ربع سکه", + "price_rial": 535000000, + "price_toman": 53500000, + "change": "(2.88%) 15,000,000", + "min_toman": 52500000, + "max_toman": 53500000, + "last_update": "15:38:23" + }, + { + "name": "سکه گرمی", + "price_rial": 275000000, + "price_toman": 27500000, + "change": "(1.85%) 5,000,000", + "min_toman": 27000000, + "max_toman": 27500000, + "last_update": "15:55:20" + } + ] +}; \ No newline at end of file diff --git a/live_prices.json b/live_prices.json new file mode 100644 index 0000000..2254c00 --- /dev/null +++ b/live_prices.json @@ -0,0 +1,50 @@ +{ + "last_fetch": "2026-06-01 16:15:58", + "prices": [ + { + "name": "سکه امامی", + "price_rial": 1845050000, + "price_toman": 184505000, + "change": "(3.07%) 55,000,000", + "min_toman": 182480000, + "max_toman": 184510000, + "last_update": "16:13:54" + }, + { + "name": "سکه بهار آزادی", + "price_rial": 1810100000, + "price_toman": 181010000, + "change": "(2.26%) 39,950,000", + "min_toman": 177980000, + "max_toman": 181020000, + "last_update": "16:14:00" + }, + { + "name": "نیم سکه", + "price_rial": 940000000, + "price_toman": 94000000, + "change": "(2.1%) 19,300,000", + "min_toman": 93500000, + "max_toman": 94000000, + "last_update": "11:10:35" + }, + { + "name": "ربع سکه", + "price_rial": 535000000, + "price_toman": 53500000, + "change": "(2.88%) 15,000,000", + "min_toman": 52500000, + "max_toman": 53500000, + "last_update": "15:38:23" + }, + { + "name": "سکه گرمی", + "price_rial": 275000000, + "price_toman": 27500000, + "change": "(1.85%) 5,000,000", + "min_toman": 27000000, + "max_toman": 27500000, + "last_update": "15:55:20" + } + ] +} \ No newline at end of file diff --git a/panel/package-lock.json b/panel/package-lock.json index 0805428..3f1f205 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,3 +1,4 @@ +<<<<<<< HEAD { "name": "andishkade-foolad-panel", "version": "1.0.0", @@ -1940,3 +1941,1947 @@ } } } +======= +{ + "name": "andishkade-foolad-panel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "andishkade-foolad-panel", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.3.0", + "cheerio": "^1.2.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "express-rate-limit": "^7.5.0", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "nanoid": "^5.0.7", + "nodemailer": "^8.0.10" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemailer": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.10.tgz", + "integrity": "sha512-BLFuSth7QtHOkBzyqTehWWyub0NTRDuK2Q2SQfnGLsrJnzyU+Yeh4WpV1eZGuARFj1xQJHIdnTuJZLP+b9R1GQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} +>>>>>>> space/main diff --git a/panel/public/app.js b/panel/public/app.js index 44351d3..97272bb 100644 --- a/panel/public/app.js +++ b/panel/public/app.js @@ -71,6 +71,7 @@ function topbarHtml(active, primaryLabel) {
@@ -84,6 +85,7 @@ function wireTabs() { btn.addEventListener('click', () => { if (btn.dataset.tab === 'articles') renderList(); else if (btn.dataset.tab === 'risks') renderRisksList(); + else if (btn.dataset.tab === 'users') renderUsersList(); }); }); $('#logoutBtn').addEventListener('click', async () => { @@ -432,4 +434,119 @@ function renderRiskEditor(risk) { }); } +// ---------- users ---------- +async function renderUsersList() { + root.innerHTML = ` + ${topbarHtml('users', '+ کاربر جدید')} +
+

کاربران پنل

+

در حال بارگذاری…

+
+ `; + wireTabs(); + $('#newBtn').addEventListener('click', () => renderUserEditor(null)); + + try { + const items = await api('/api/users'); + if (!items.length) { + $('#list').innerHTML = `

هنوز کاربری ثبت نشده است.

`; + return; + } + $('#list').innerHTML = items.map(u => ` +
+
+
+ ${escapeHtml(u.username)} + ${u.createdAt ? `عضویت: ${escapeHtml(u.createdAt)}` : ''} +
+
+
+ + +
+
+ `).join(''); + + $('#list').addEventListener('click', async (e) => { + const btn = e.target.closest('button[data-action]'); + if (!btn) return; + const row = btn.closest('.row'); + const id = row.dataset.id; + if (btn.dataset.action === 'edit') { + renderUserEditor({ id, username: row.dataset.username }); + } else if (btn.dataset.action === 'delete') { + if (!confirm('این کاربر حذف شود؟')) return; + try { + await api(`/api/users/${id}`, { method: 'DELETE' }); + renderUsersList(); + } catch (err) { + alert('حذف ناموفق بود: ' + friendlyUserError(err.message)); + } + } + }); + } catch (err) { + console.error(err); + $('#list').innerHTML = `

خطا در بارگذاری کاربران.

`; + } +} + +function renderUserEditor(user) { + const u = user || { username: '' }; + const isEdit = !!user; + + root.innerHTML = ` +
+
${isEdit ? 'ویرایش کاربر' : 'کاربر جدید'}
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ `; + + $('#backBtn').addEventListener('click', () => renderUsersList()); + + $('#editorForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const fd = new FormData(e.target); + const username = String(fd.get('username') || '').trim(); + const password = String(fd.get('password') || ''); + const payload = { username }; + if (password) payload.password = password; + try { + if (isEdit) await api(`/api/users/${user.id}`, { method: 'PUT', body: payload }); + else await api('/api/users', { method: 'POST', body: { username, password } }); + renderUsersList(); + } catch (err) { + alert('ذخیره ناموفق بود: ' + friendlyUserError(err.message)); + } + }); +} + +function friendlyUserError(msg) { + const m = String(msg || ''); + if (m.includes('username_taken')) return 'این نام کاربری قبلاً ثبت شده است.'; + if (m.includes('username_too_short')) return 'نام کاربری باید حداقل ۳ کاراکتر باشد.'; + if (m.includes('password_too_short')) return 'رمز عبور باید حداقل ۸ کاراکتر باشد.'; + if (m.includes('cannot_delete_self')) return 'نمی‌توانید حساب خودتان را حذف کنید.'; + if (m.includes('cannot_delete_last_user')) return 'آخرین کاربر را نمی‌توان حذف کرد.'; + return m; +} + api('/api/auth/me').then(() => renderList()).catch(() => renderLogin()); diff --git a/panel/server.js b/panel/server.js index c807b80..779c4f1 100644 --- a/panel/server.js +++ b/panel/server.js @@ -280,6 +280,57 @@ app.delete('/api/risks/:id', authRequired, (req, res) => { res.status(204).end(); }); +// ---------- users (admin management) ---------- +// password_hash is NEVER returned to the client. +app.get('/api/users', authRequired, (_req, res) => { + const rows = db.prepare('SELECT id, username, created_at FROM users ORDER BY id').all(); + res.json(rows.map((r) => ({ id: r.id, username: r.username, createdAt: r.created_at }))); +}); + +app.post('/api/users', authRequired, (req, res) => { + const username = String(req.body?.username || '').trim(); + const password = String(req.body?.password || ''); + if (username.length < 3) return res.status(400).json({ error: 'username_too_short' }); + if (password.length < 8) return res.status(400).json({ error: 'password_too_short' }); + const exists = db.prepare('SELECT 1 FROM users WHERE username = ?').get(username); + if (exists) return res.status(409).json({ error: 'username_taken' }); + const hash = bcrypt.hashSync(password, 10); + const info = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)').run(username, hash); + res.status(201).json({ id: info.lastInsertRowid, username }); +}); + +app.put('/api/users/:id', authRequired, (req, res) => { + const id = Number(req.params.id); + const row = db.prepare('SELECT id, username FROM users WHERE id = ?').get(id); + if (!row) return res.status(404).json({ error: 'not_found' }); + + const username = req.body?.username != null ? String(req.body.username).trim() : row.username; + const password = req.body?.password != null ? String(req.body.password) : ''; + if (username.length < 3) return res.status(400).json({ error: 'username_too_short' }); + if (username !== row.username) { + const taken = db.prepare('SELECT 1 FROM users WHERE username = ? AND id != ?').get(username, id); + if (taken) return res.status(409).json({ error: 'username_taken' }); + } + if (password) { + if (password.length < 8) return res.status(400).json({ error: 'password_too_short' }); + const hash = bcrypt.hashSync(password, 10); + db.prepare('UPDATE users SET username = ?, password_hash = ? WHERE id = ?').run(username, hash, id); + } else { + db.prepare('UPDATE users SET username = ? WHERE id = ?').run(username, id); + } + res.json({ id, username }); +}); + +app.delete('/api/users/:id', authRequired, (req, res) => { + const id = Number(req.params.id); + if (req.user?.sub === id) return res.status(400).json({ error: 'cannot_delete_self' }); + const count = db.prepare('SELECT COUNT(*) AS n FROM users').get().n; + if (count <= 1) return res.status(400).json({ error: 'cannot_delete_last_user' }); + const info = db.prepare('DELETE FROM users WHERE id = ?').run(id); + if (info.changes === 0) return res.status(404).json({ error: 'not_found' }); + res.status(204).end(); +}); + // ---------- prices (scraped from tgju.org every 2 min) ---------- app.get('/api/prices', (_req, res) => { const rows = db diff --git a/public/Frame 2095585206 (2).png b/public/Frame 2095585206 (2).png new file mode 100644 index 0000000..c158479 Binary files /dev/null and b/public/Frame 2095585206 (2).png differ diff --git a/public/Horizontal_Book_Mockup_6 1.webp b/public/Horizontal_Book_Mockup_6 1.webp new file mode 100644 index 0000000..5928761 Binary files /dev/null and b/public/Horizontal_Book_Mockup_6 1.webp differ diff --git a/public/logo white.png b/public/logo white.png new file mode 100644 index 0000000..c5b3ee6 Binary files /dev/null and b/public/logo white.png differ diff --git a/scraper/scraper.py b/scraper/scraper.py new file mode 100644 index 0000000..fecb26f --- /dev/null +++ b/scraper/scraper.py @@ -0,0 +1,300 @@ +import os +import sys +import urllib.request +import ssl +import re +import time +import json +from datetime import datetime + +# Ensure terminal output supports UTF-8 on Windows +if sys.platform == 'win32': + try: + sys.stdout.reconfigure(encoding='utf-8') + except AttributeError: + pass + +# Persian/Arabic to English digit mapping +FA_TO_EN = { + '۰': '0', '۱': '1', '۲': '2', '۳': '3', '۴': '4', + '۵': '5', '۶': '6', '۷': '7', '۸': '8', '۹': '9', + '٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4', + '٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9' +} + +def clean_persian_text(text): + """Clean HTML tags and standardize spacing.""" + if not text: + return "" + text = re.sub(r'<[^>]+>', '', text) + text = re.sub(r'\s+', ' ', text).strip() + return text + +def to_english_digits(text): + """Convert Persian/Arabic digits in a string to English digits.""" + if not text: + return "" + return "".join(FA_TO_EN.get(char, char) for char in text) + +def parse_numeric(text): + """Convert a price string with commas and Persian digits into an integer.""" + if not text: + return 0 + clean_text = to_english_digits(text) + digits_only = re.sub(r'[^\d]', '', clean_text) + try: + return int(digits_only) if digits_only else 0 + except ValueError: + return 0 + +def fetch_html_with_retry(url, retries=3, delay=2): + """Fetch URL with retries, custom headers, and disabled SSL checks.""" + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'fa,en-US;q=0.9,en;q=0.8', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + + # Make it a POST request by passing empty data (b"") to completely bypass CDN/ArvanCloud HTML caching + req = urllib.request.Request(url, data=b"", headers=headers) + + for attempt in range(1, retries + 1): + try: + with urllib.request.urlopen(req, context=ctx, timeout=10) as response: + if response.status == 200: + html = response.read().decode('utf-8') + if "sekee" in html: + return html + else: + raise ValueError("Incorrect page content returned (missing expected coin tags).") + except Exception as e: + if attempt < retries: + time.sleep(delay) + delay *= 1.5 + else: + print(f"⚠️ Connection error on attempt {attempt}: {e}") + return None + +HISTORY_LOG = [] +HISTORY_INITIALIZED = False + +def initialize_history_from_excel(): + global HISTORY_LOG, HISTORY_INITIALIZED + excel_filename = "coin_prices.xlsx" + if not HISTORY_INITIALIZED: + if os.path.exists(excel_filename): + try: + import pandas as pd + df = pd.read_excel(excel_filename, sheet_name="تاریخچه تغییرات") + HISTORY_LOG = df.to_dict(orient="records") + print(f"📥 Loaded {len(HISTORY_LOG)} history logs from existing '{excel_filename}'.") + except Exception as e: + print(f"💡 Initialized new history log in memory (failed to load: {e})") + HISTORY_INITIALIZED = True + +def save_live_js_and_json(scraped_data): + """Save scraped data to JSON and JS files for real-time HTML dashboard.""" + current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + live_records = [] + for row in scraped_data: + live_records.append({ + "name": row["Name"], + "price_rial": row["Price_Rial"], + "price_toman": row["Price_Toman"], + "change": row["Change"], + "min_toman": row["Min_Price_Toman"], + "max_toman": row["Max_Price_Toman"], + "last_update": row["Last_Update"] + }) + + data_to_save = { + "last_fetch": current_time_str, + "prices": live_records + } + + # Save as JSON + try: + with open("live_prices.json", "w", encoding="utf-8") as f: + json.dump(data_to_save, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"⚠️ Error saving live_prices.json: {e}") + + # Save as JS (for file:// protocol CORS bypass) + try: + js_content = f"window.LIVE_COIN_PRICES = {json.dumps(data_to_save, ensure_ascii=False, indent=2)};" + with open("live_prices.js", "w", encoding="utf-8") as f: + f.write(js_content) + except Exception as e: + print(f"⚠️ Error saving live_prices.js: {e}") + +def save_to_excel(scraped_data): + """Save scraped data to Excel file with Latest Prices and History sheets.""" + global HISTORY_LOG + excel_filename = "coin_prices.xlsx" + + # Initialize history from Excel if we haven't already + initialize_history_from_excel() + + # Create list for current scraped data + new_rows = [] + current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + for row in scraped_data: + new_rows.append({ + "زمان ثبت": current_time_str, + "نام سکه": row["Name"], + "قیمت فعلی (ریال)": row["Price_Rial"], + "قیمت فعلی (تومان)": row["Price_Toman"], + "میزان و درصد تغییر": row["Change"], + "کمترین قیمت روز (تومان)": row["Min_Price_Toman"], + "بیشترین قیمت روز (تومان)": row["Max_Price_Toman"], + "زمان به‌روزرسانی سایت": row["Last_Update"] + }) + + import pandas as pd + df_new = pd.DataFrame(new_rows) + df_latest = df_new.drop(columns=["زمان ثبت"]) + + # Accumulate history in memory list + temp_history_log = HISTORY_LOG + new_rows + df_history = pd.DataFrame(temp_history_log) + + # Write both sheets to Excel + try: + with pd.ExcelWriter(excel_filename, engine='openpyxl') as writer: + df_latest.to_excel(writer, sheet_name="قیمت‌های لحظه‌ای", index=False) + df_history.to_excel(writer, sheet_name="تاریخچه تغییرات", index=False) + + # Write succeeded! Commit the temp log to our persistent memory HISTORY_LOG + HISTORY_LOG = temp_history_log + print(f"📊 Excel file updated: '{excel_filename}' (Latest & History sheets)") + return True + except Exception as e: + print(f"⚠️ Warning: Excel file is locked (likely open in Microsoft Excel).") + print(f"⚠️ {len(new_rows)} rows kept in memory and will auto-save on the next minute cycle when you close Excel. (Error: {e})") + return False + +def run_scraper_cycle(): + """Runs a single cycle of the scraper.""" + url = "https://www.tgju.org/coin" + html = fetch_html_with_retry(url) + + if not html: + print("📡 Connection to TGJU failed. Retrying in next cycle...") + return False + + market_rows = re.findall(r']*data-market-row="([^"]+)"[^>]*>([\s\S]*?)<\/tr>', html) + + target_keys = { + "sekee": "سکه امامی", + "sekeb": "سکه بهار آزادی", + "nim": "نیم سکه", + "rob": "ربع سکه", + "gerami": "سکه گرمی" + } + + scraped_data = [] + + for row_name, row_content in market_rows: + if row_name in target_keys: + coin_display_name = target_keys[row_name] + tds = re.findall(r']*>([\s\S]*?)<\/td>', row_content) + + if len(tds) >= 5: + raw_price_rial = clean_persian_text(tds[0]) + raw_change = clean_persian_text(tds[1]) + raw_min = clean_persian_text(tds[2]) + raw_max = clean_persian_text(tds[3]) + raw_time = clean_persian_text(tds[4]) + + price_rial = parse_numeric(raw_price_rial) + price_toman = price_rial // 10 + + min_rial = parse_numeric(raw_min) + min_toman = min_rial // 10 + + max_rial = parse_numeric(raw_max) + max_toman = max_rial // 10 + + clean_change = to_english_digits(raw_change) + clean_time = to_english_digits(raw_time) + + scraped_data.append({ + "Key": row_name, + "Name": coin_display_name, + "Price_Rial": price_rial, + "Price_Toman": price_toman, + "Change": clean_change, + "Min_Price_Toman": min_toman, + "Max_Price_Toman": max_toman, + "Last_Update": clean_time + }) + + if not scraped_data: + print("⚠️ No coin rows were matched in the HTML. Table structure may have changed.") + return False + + # Save Excel + try: + save_to_excel(scraped_data) + except Exception as e: + print(f"⚠️ Error preparing Excel: {e}") + + # Save JSON and JS for the dynamic HTML dashboard + try: + save_live_js_and_json(scraped_data) + except Exception as e: + print(f"⚠️ Error saving live assets: {e}") + + # Print safely to console + print("\n" + "=" * 80) + print(f"{'نام سکه':<20} | {'قیمت (تومان)':<18} | {'تغییر':<18} | {'به‌روزرسانی':<12}") + print("-" * 80) + for row in scraped_data: + name_ascii = row["Name"].ljust(20) + price_str = f"{row['Price_Toman']:,}" + change_str = row["Change"] + time_str = row["Last_Update"] + try: + print(f"{name_ascii} | {price_str:<18} | {change_str:<18} | {time_str:<12}") + except Exception: + print(f"{row['Key'].ljust(20)} | {price_str:<18} | {change_str:<18} | {time_str:<12}") + print("=" * 80 + "\n") + return True + +def main(): + print("=" * 60) + print(" 🪙 TGJU AUTOMATED COIN SCRAPER & SCHEDULER 🪙") + print(" 🔄 Running every 1 minute | Output: Excel & Realtime HTML") + print("=" * 60) + + print(f"🚀 Initializing scraper at {datetime.now().strftime('%H:%M:%S')}...") + run_scraper_cycle() + + while True: + try: + for remaining in range(60, 0, -1): + sys.stdout.write(f"\r⏳ Next fetch in {remaining:02d} seconds... ") + sys.stdout.flush() + time.sleep(1) + sys.stdout.write("\r📡 Fetching live prices... \n") + sys.stdout.flush() + + run_scraper_cycle() + except KeyboardInterrupt: + print("\n👋 Scraper stopped by user. Goodbye!") + sys.exit(0) + except Exception as e: + print(f"\n⚠️ Unexpected error in loop: {e}") + print("⏳ Retrying in 10 seconds...") + time.sleep(10) + +if __name__ == "__main__": + main() diff --git a/spread-00.png b/spread-00.png new file mode 100644 index 0000000..094a495 Binary files /dev/null and b/spread-00.png differ diff --git a/spread-450.png b/spread-450.png new file mode 100644 index 0000000..55186a7 Binary files /dev/null and b/spread-450.png differ diff --git a/spread-850.png b/spread-850.png new file mode 100644 index 0000000..ac40f5f Binary files /dev/null and b/spread-850.png differ diff --git a/src/components/ui/StrategicEvents.tsx b/src/components/ui/StrategicEvents.tsx new file mode 100644 index 0000000..f49ae12 --- /dev/null +++ b/src/components/ui/StrategicEvents.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import { ChevronDown } from 'lucide-react' + +const NAVY = '#032340' +const GOLD = '#CD9E53' + +type Ev = { title: string; desc: string; when: string } + +const EVENTS: Ev[] = [ + { + title: 'اجلاس جهانی فولاد سبز | استکهلم، سوئد', + desc: 'بررسی آخرین تکنولوژی‌های احیای مستقیم بر پایه هیدروژن و جذب سرمایه برای پروژه‌های بدون کربن. حضور در این رویداد برای درک آینده مقررات زیست‌محیطی اروپا حیاتی است.', + when: 'مهر ۱۴۰۵', + }, + { + title: 'نشست سالانه انجمن جهانی فولاد (worldsteel) | شانگهای، چین', + desc: 'گردهمایی بزرگ‌ترین رهبران صنعت. محور اصلی امسال: پیش‌بینی تقاضای جهانی در سایه رکود بخش مسکن چین و تغییر مسیر زنجیره تامین به سمت هند و خاورمیانه.', + when: 'آبان ۱۴۰۵', + }, + { + title: 'مجمع بین‌المللی هوش مصنوعی در تولید | برلین، آلمان', + desc: 'رونمایی از جدیدترین نسل دوقلوهای دیجیتال (Digital Twins) و الگوریتم‌های پیش‌بینی تعمیرات. فرصتی برای آشنایی با راهکارهای کاهش هزینه و افزایش بهره‌وری خطوط نورد.', + when: 'آذر ۱۴۰۵', + }, +] + +export default function StrategicEvents() { + const [open, setOpen] = useState(0) + + return ( +
+ {/* heading */} +
+
+

+ دیده‌بان رویدادهای راهبردی +

+
+ + {/* accordion list */} +
+ {EVENTS.map((e, i) => { + const isOpen = open === i + return ( +
+ + + {/* expandable body */} +
+

+ {e.desc} (زمان: {e.when}) +

+
+
+ ) + })} +
+
+ ) +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index f4d4962..a21f0ba 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -2,6 +2,8 @@ import SpreadFeatures from './sections/SpreadFeatures' import IntegrationsSection from './sections/IntegrationsSection' // import InternalNewsSection from './sections/InternalNewsSection' // «تازه‌ترین اخبار صنعت» — disabled, restore anytime import RadarTechSection from './sections/RadarTechSection' +import AtAGlanceSection from './sections/AtAGlanceSection' +import FeaturedReportBanner from './sections/FeaturedReportBanner' import MapEventsSection from './sections/MapEventsSection' import FactoryReportsScroll from './sections/FactoryReportsScroll' import NewsletterMarquee from './sections/NewsletterMarquee' @@ -16,7 +18,10 @@ export default function Home() {
{/* «تازه‌ترین اخبار صنعت» — disabled, restore anytime */} - + + + + diff --git a/src/pages/Home/sections/AtAGlanceSection.tsx b/src/pages/Home/sections/AtAGlanceSection.tsx index 926438a..842d149 100644 --- a/src/pages/Home/sections/AtAGlanceSection.tsx +++ b/src/pages/Home/sections/AtAGlanceSection.tsx @@ -60,27 +60,27 @@ export default function AtAGlanceSection({ embedded = false }: { embedded?: bool } return ( -
+
{/* heading with gold rule (hidden when embedded in a page that has its own header) */} {!embedded && ( -
-

+
+

{T.heading}

-
+
)} - {/* image on the LEFT, text on the RIGHT in RTL (matches the reference). - In RTL, the first flex child lands on the right — so we use row-reverse to push image left. */} + {/* image on the LEFT, text on the RIGHT (matches the reference). + Container is LTR; image is the first DOM child, so plain `row` lands it on the left. */}
{/* ── IMAGE side (with stacked-card shadow) ── */} -
+
{/* back card (offset, behind) — offset away from the text card */}
{ /* TODO: wire real PDF when available */ } + + return ( +
+
+ {/* wrapper allows the book to spill outside the frame */} +
+ + {/* clipped background (navy gradient + angled gold band) */} +
+
+
+ + {/* content: text on the LEFT, book on the RIGHT (text is first DOM child, plain row) */} +
+ + {/* TEXT — left */} +
+
{t.overline}
+

+ {t.title} +

+
+ {t.subtitle} +
+ {/* button BELOW the text */} + +
+ + {/* BOOK — right, spilling out of the frame */} +
+ {t.title} +
+ +
+
+
+
+ ) +} diff --git a/src/pages/Home/sections/MapEventsSection.tsx b/src/pages/Home/sections/MapEventsSection.tsx index 1b2258d..6beaadc 100644 --- a/src/pages/Home/sections/MapEventsSection.tsx +++ b/src/pages/Home/sections/MapEventsSection.tsx @@ -1,52 +1,18 @@ -import { Link } from 'react-router-dom' import Globe from '@/components/ui/globe' -import EventCalendar from '@/components/ui/EventCalendar' +import StrategicEvents from '@/components/ui/StrategicEvents' import { useLang } from '@/context/LangContext' -const T = { - fa: { - mapOverline: 'اسکنر جهانی', - mapHeading: 'تمام رویدادهای فولادی دنیا را رصد می‌کنیم', - mapSub: 'از بورس دالیان چین تا گمرک بصره عراق — هر سیگنالی که بر رقابت‌پذیری ایران اثر می‌گذارد.', - eventsOverline: 'تقویم رویداد', eventsHeading: 'رویدادهای پیش‌رو', - eventsAll: 'تقویم کامل ←', register: 'ثبت‌نام ←', soon: 'به‌زودی', - }, - en: { - mapOverline: 'GLOBAL SCANNER', - mapHeading: 'Every Global Steel Event, Monitored', - mapSub: 'From the Dalian Commodity Exchange to the Basra customs gate — every signal affecting Iran\'s competitiveness.', - eventsOverline: 'EVENT CALENDAR', eventsHeading: 'Upcoming Events', - eventsAll: 'Full Calendar →', register: 'Register →', soon: 'Coming Soon', - }, -} - export default function MapEventsSection() { const { lang } = useLang() - const t = T[lang] return (
- {/* ── Text + Calendar — RIGHT in RTL ── */} -
- {/* scanner heading (moved here from above the globe) */} -
-
{t.mapOverline}
-

{t.mapHeading}

-

{t.mapSub}

-
- {/* upcoming-events heading */} -
-
-

{t.eventsHeading}

- {t.eventsAll} -
-
-
- -
+ {/* ── Strategic events — RIGHT in RTL ── */} +
+
{/* ── Globe — LEFT, no text ── */} diff --git a/src/pages/Home/sections/RadarTechSection.tsx b/src/pages/Home/sections/RadarTechSection.tsx index 8aa6558..d74d894 100644 --- a/src/pages/Home/sections/RadarTechSection.tsx +++ b/src/pages/Home/sections/RadarTechSection.tsx @@ -40,15 +40,18 @@ const T = { }, } -export default function RadarTechSection() { +export default function RadarTechSection({ only }: { only?: 'radar' | 'approach' }) { const { lang } = useLang() const t = T[lang] + const showRadar = only !== 'approach' + const showApproach = only !== 'radar' return (
{/* ══ رادار آینده ══ */} + {showRadar && (
@@ -65,11 +68,13 @@ export default function RadarTechSection() { ))}
+ )} - {/* divider */} -
+ {/* divider — only when both halves render together */} + {showRadar && showApproach &&
} {/* ══ حوزه‌های پژوهشی ══ */} + {showApproach && (
@@ -108,6 +113,7 @@ export default function RadarTechSection() { })}
+ )}
diff --git a/src/pages/Home/sections/VideocastPodcastSection.tsx b/src/pages/Home/sections/VideocastPodcastSection.tsx index e6571ea..29567e5 100644 --- a/src/pages/Home/sections/VideocastPodcastSection.tsx +++ b/src/pages/Home/sections/VideocastPodcastSection.tsx @@ -1,51 +1,72 @@ import { useState } from 'react' import { useLang } from '@/context/LangContext' +const NAVY = '#032340' +const GOLD = '#CD9E53' + +const HASHTAGS = ['#فولاد _ آینده', '#فولاد _ آینده', '#فولاد _ آینده'] + const VIDEOS = [ { id: 1, - fa: { title: 'چشم‌انداز فولاد ایران در ۱۴۰۴', desc: 'بررسی روندهای تولید، صادرات و سیاست‌گذاری صنعت فولاد ایران در سال پیش رو' }, - en: { title: 'Iran Steel Outlook 1404', desc: 'Production trends, exports and policy analysis for Iran\'s steel industry' }, - ep: 'EP.12', duration: '۴۲ دقیقه', thumb: '/news/photo-1504307651254-35680f356dfd.jpg', + fa: { title: 'نگاهی به آن سوی مرزهای تولید', guest: 'گفتگو با مهندس سیامک فجری', + body: 'در ویدیوکست «Steel Horizon»، ما فراتر از داده‌های خام قیمت و تناژ، به سراغ مهندسی تغییر و معماری آینده صنعت می‌رویم. اینجا محل تلاقی استراتژی‌های کلان، تکنولوژی‌های برهم‌زننده و رهبرانی است که فولاد فردا را نه با کوره‌ها، که با «بینش» می‌سازند. ما طوفان‌های بازار را تحلیل می‌کنیم تا شما با اطمینان، مسیر آینده را ترسیم کنید.\nصنعت فولاد در حال ورود به پیچیده‌ترین چرخه حیات خود است؛ جایی که هوش مصنوعی، مقررات کربنی و ژئواکونومی، قواعد بازی را بازنویسی کرده‌اند. در «Steel Logic» با تحلیل‌گران ارشد و استراتژیست‌های تراز اول، به گفتگو می‌نشینیم تا لایه‌های پنهان این پیچیدگی‌ها را آشکار کنیم. اینجا خبری از هیجان‌زدگی نیست؛ فقط تحلیل‌های داده‌محور و تصمیم‌ساز.' }, + en: { title: 'Beyond the Frontiers of Production', guest: 'A talk with Eng. Siamak Fajri', + body: 'In the "Steel Horizon" videocast we go beyond raw price and tonnage data to the engineering of change and the architecture of the industry\'s future.' }, + ep: 'EP.12', duration: '۴۲ دقیقه', date: '۲۵ خرداد ۱۴۰۵', thumb: '/news/photo-1504307651254-35680f356dfd.jpg', }, { id: 2, - fa: { title: 'تحلیل بازار جهانی فلزات پایه', desc: 'نوسانات قیمت مس، آلومینیوم و روی و تأثیر آن بر زنجیره فولاد' }, - en: { title: 'Global Base Metals Analysis', desc: 'Price volatility in copper, aluminum and zinc and its steel chain impact' }, - ep: 'EP.11', duration: '۳۸ دقیقه', thumb: '/news/photo-1558618666-fcd25c85cd64.jpg', + fa: { title: 'تحلیل بازار جهانی فلزات پایه', guest: 'گفتگو با کارشناسان بازار', + body: 'نوسانات قیمت مس، آلومینیوم و روی و تأثیر آن بر زنجیره فولاد را با تحلیل‌گران بازار بررسی می‌کنیم.' }, + en: { title: 'Global Base Metals Analysis', guest: 'With market analysts', + body: 'Price volatility in copper, aluminum and zinc and its steel-chain impact.' }, + ep: 'EP.11', duration: '۳۸ دقیقه', date: '۱۸ خرداد ۱۴۰۵', thumb: '/news/photo-1558618666-fcd25c85cd64.jpg', }, { id: 3, - fa: { title: 'ژئوپلیتیک و صادرات فولاد', desc: 'تأثیر تحولات منطقه‌ای بر بازارهای صادراتی فولاد ایران و فرصت‌های پیش رو' }, - en: { title: 'Geopolitics & Steel Exports', desc: 'How regional developments shape Iran\'s steel export markets' }, - ep: 'EP.10', duration: '۵۱ دقیقه', thumb: '/news/photo-1504328345606-18bbc8c9d7d1.jpg', + fa: { title: 'ژئوپلیتیک و صادرات فولاد', guest: 'گفتگو با تحلیل‌گران ژئواکونومی', + body: 'تأثیر تحولات منطقه‌ای بر بازارهای صادراتی فولاد ایران و فرصت‌های پیش رو.' }, + en: { title: 'Geopolitics & Steel Exports', guest: 'With geo-economics analysts', + body: 'How regional developments shape Iran\'s steel export markets.' }, + ep: 'EP.10', duration: '۵۱ دقیقه', date: '۱۱ خرداد ۱۴۰۵', thumb: '/news/photo-1504328345606-18bbc8c9d7d1.jpg', }, { id: 4, - fa: { title: 'سرمایه‌گذاری در صنعت فولاد', desc: 'فرصت‌ها و چالش‌های سرمایه‌گذاری در بخش‌های مختلف زنجیره ارزش فولاد' }, - en: { title: 'Steel Industry Investment', desc: 'Opportunities and challenges across the steel value chain' }, - ep: 'EP.09', duration: '۴۵ دقیقه', thumb: '/news/photo-1611273426858-450d8e3c9fce.jpg', + fa: { title: 'سرمایه‌گذاری در صنعت فولاد', guest: 'گفتگو با مدیران سرمایه‌گذاری', + body: 'فرصت‌ها و چالش‌های سرمایه‌گذاری در بخش‌های مختلف زنجیره ارزش فولاد.' }, + en: { title: 'Steel Industry Investment', guest: 'With investment managers', + body: 'Opportunities and challenges across the steel value chain.' }, + ep: 'EP.09', duration: '۴۵ دقیقه', date: '۴ خرداد ۱۴۰۵', thumb: '/news/photo-1611273426858-450d8e3c9fce.jpg', }, ] +const POD_HASHTAGS = ['#هوش _ مصنوعی', '#عدم _ قطعیت'] + const PODCASTS = [ { id: 1, - fa: { title: 'نبض صنعت — فصل سوم', ep: 'قسمت ۱۲', desc: 'گفتگو با مدیران ارشد صنعت فولاد درباره چالش‌های تامین مالی و توسعه ظرفیت' }, - en: { title: 'Industry Pulse — Season 3', ep: 'Episode 12', desc: 'Steel executives on financing challenges and capacity expansion' }, - duration: '۵۸ دقیقه', cover: '/news/photo-1540575467063-178a50c2df87.jpg', + epNum: 'اپیزود ۷۷', date: '۲۵ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱۰:۵۶', cover: '/news/photo-1540575467063-178a50c2df87.jpg', + fa: { title: 'کالبدشکافی عقلانی پیچیدگی‌های صنعتی', ep: 'قسمت ۱۲', guest: 'مهندس سیامک شجاعی', + body: '«صنعت فولاد در عصر حاضر، در پیچیده‌ترین چرخه حیات خود قرار گرفته است؛ جایی که همگرایی هوش مصنوعی، الزامات زیست‌محیطی و تنش‌های ژئواکونومیک، قواعد کلاسیک تولید و تجارت را به‌کلی بازنویسی کرده‌اند. در پادکست «نام انتخابی»، ما با عبور از هیاهوی رسانه‌ای و تحلیل‌های سطحی، به کالبدشکافی عقلانی این پیچیدگی‌ها می‌پردازیم. ما با دعوت از استراتژیست‌های تراز اول و خبرگان این حوزه، لایه‌های پنهان چالش‌های صنعتی را می‌شکافیم تا بینشی شفاف، داده‌محور و تصمیم‌ساز را در اختیار رهبری قرار دهیم که برای عبور از طوفان‌های تغییر، به قطب‌نمای دقیق‌تر از عرف‌های بازار نیاز دارند.»' }, + en: { title: 'A Rational Dissection of Industrial Complexity', ep: 'Episode 12', guest: 'Eng. Siamak Shojaei', + body: 'Steel today is in its most complex cycle yet — where AI, environmental mandates and geo-economic tension rewrite the classic rules of production and trade.' }, }, { id: 2, - fa: { title: 'صدای بازار', ep: 'قسمت ۸', desc: 'تحلیل هفتگی قیمت‌های شمش، میلگرد و مقاطع با کارشناسان بازار' }, - en: { title: 'Market Voice', ep: 'Episode 8', desc: 'Weekly analysis of billet, rebar and section prices with market experts' }, - duration: '۳۲ دقیقه', cover: '/news/photo-1566873535350-6586b0b7a1f2.jpg', + epNum: 'اپیزود ۷۶', date: '۱۸ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱:۴۰:۱۸', cover: '/news/photo-1566873535350-6586b0b7a1f2.jpg', + fa: { title: 'آزمایشگاه کوره', ep: 'قسمت ۸', guest: 'کارشناسان بازار', + body: '«The Foundry Lab» محلی برای کالبدشکافی دقیق تحولات فناورانه و مدل‌های نوین فولادسازی است. ما در این پادکست با نگاهی موشکافانه، چالش‌های فنی را بررسی می‌کنیم.' }, + en: { title: 'The Foundry Lab', ep: 'Episode 8', guest: 'Market experts', + body: 'The Foundry Lab dissects technological change and new steelmaking models with a meticulous engineering lens.' }, }, { id: 3, - fa: { title: 'آینده سبز فولاد', ep: 'قسمت ۵', desc: 'مسیر صنعت فولاد ایران به سمت کاهش کربن و پایداری' }, - en: { title: 'Green Steel Future', ep: 'Episode 5', desc: 'Iran\'s steel sector path toward carbon reduction and sustainability' }, - duration: '۴۴ دقیقه', cover: '/news/photo-1578662996442-48f60103fc96.jpg', + epNum: 'اپیزود ۷۵', date: '۱۱ / خرداد / ۱۴۰۵', rating: '۴/۵', duration: '۱:۴۰:۱۸', cover: '/news/photo-1578662996442-48f60103fc96.jpg', + fa: { title: 'معمار فولاد', ep: 'قسمت ۵', guest: 'رهبران صنعت', + body: 'در «Steel Architect»، ما به سراغ داستان ساختن فولاد فردا می‌رویم؛ روایتی که رهبرانی که از مدل‌های سنتی کسب‌وکار به چالش کشیده‌اند را به تصویر می‌کشد.' }, + en: { title: 'Steel Architect', ep: 'Episode 5', guest: 'Industry leaders', + body: 'Steel Architect tells the story of building tomorrow\'s steel — leaders challenging traditional business models.' }, }, ] @@ -79,168 +100,216 @@ export default function VideocastPodcastSection({ only }: { only?: 'video' | 'po
- {/* ── VIDEOCAST 70% ── */} + {/* ── VIDEOCAST (full-width featured player + playlist + description) ── */} {showVideo && ( -
+
{/* heading */} -
-
-
-
- VIDEOCAST -
-

{t.videoLabel}

-

{t.videoSub}

-
- +
+
+

{t.videoLabel}

- {/* featured */} -
- -
- {/* play */} -
+ +
+ {/* center play */} + + {/* controls bar */} +
+
+
+ +
+
+
+
+ + + 🔊 + ۰۹:۲۷ / {VIDEOS[activeVideo].duration} +
+
+ + +
-

{VIDEOS[activeVideo][lang].title}

-

{VIDEOS[activeVideo][lang].desc}

- {/* episode cards */} -
- {VIDEOS.map((v, i) => ( - - ))} + {/* playlist (right) + description (left) */} +
+ + {/* playlist — first DOM child → right in RTL */} +
+
+ پلی لیست + 🎬 +
+
+ {VIDEOS.map((v, i) => ( + + ))} +
+
+ + {/* description — second DOM child → left in RTL */} +
+
+ 🎙 +

{VIDEOS[activeVideo][lang].title}

+
+

+ {VIDEOS[activeVideo][lang].body} +

+
+ {HASHTAGS.map((h, i) => ( + {h} + ))} +
+
)} {/* ── PODCAST (full width) ── */} {showPod && ( -
+
+
{/* heading */} -
-
-
-
- PODCAST -
-

{t.podLabel}

-

{t.podSub}

+
+
+
+

{t.podLabel}

-
- {/* featured player (start) + episode list (end) */} -
- - {/* ── featured episode player card ── */} -
- {/* soft glow */} -
-
- -
-
{pod[lang].ep}
-

{pod[lang].title}

-
-
-

{pod[lang].desc}

- - {/* progress */} -
-
-
-
-
- ۲۰:۱۲{pod.duration} -
-
- - {/* controls */} -
- - - + {/* ── featured player card (light) ── */} +
+ {/* cover + rating — first DOM child → right in RTL */} +
+ +
+ ★★★★★ + {pod.rating}
- {/* ── episode list ── */} -
+ {/* player body — left */} +
+ {/* top icons + episode + title */} +
+
+ + 🔖 +
+
+
{pod.epNum}
+

{pod[lang].title}

+
+
+ + {/* play + date */} +
+ +
+
+ {pod.date} 📅 +
+
+ + {/* waveform + times */} +
+ ۰۲:۳۱ +
+ {Array.from({ length: 64 }).map((_, i) => { + const h = 5 + Math.abs(Math.sin(i * 1.7)) * 18 + return + })} +
+ {pod.duration} +
+
+
+ + {/* ── episode list (left) + description (right) ── */} +
+ + {/* episode list — first DOM → right in RTL */} +
{PODCASTS.map((p, i) => ( ))}
+ + {/* description — second DOM → left in RTL */} +
+
+ 🎙 +

{pod[lang].title}

+
+

{pod[lang].body}

+
مهمان برنامه: {pod[lang].guest}
+
+ {POD_HASHTAGS.map((h, i) => {h})} +
+
+
+
)}