diff --git a/api-dashboard/.gitignore b/api-dashboard/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/api-dashboard/.gitignore
@@ -0,0 +1,24 @@
+# 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?
diff --git a/api-dashboard/README.md b/api-dashboard/README.md
new file mode 100644
index 00000000..34b91ede
--- /dev/null
+++ b/api-dashboard/README.md
@@ -0,0 +1,49 @@
+# Tillo Developer Console (API Dashboard)
+
+Welcome to the **Tillo Developer Console**, a premium management dashboard and integration interface for creating, managing, and testing developer API Keys for external system widgets and program modules.
+
+## 🏗️ Architecture & Security Model
+
+To protect sensitive canteen financials and limit key usage footprint, this module operates under a robust security architecture:
+
+1. **Usage Quota Limits**: Each system administrator or customer user is strictly limited to obtaining a maximum of **3 active API keys**.
+2. **Access Control Scopes**: API Keys obtain a specialized **Read-Only** access scope. Write operations, wallet top-ups, session creations, or master configurations are rejected with a HTTP `401 Unauthorized` or `403 Forbidden` response.
+3. **Financial Exclusion Guard**: All developer API key requests are barred from viewing financial statistics (e.g. daily store revenue, hourly billing graphs, transaction tables, master ledgers, and vendor settlement logs). General non-sensitive counters like active product pricing lists, public store stalls, and order statuses are open for querying.
+4. **Wallet Safeguard**:
+ - **Customer API Keys** can query the active wallet balance of the owner user account.
+ - **System API Keys** can query general system token circulation statistics, but cannot access individual customer transaction lines or private ledger balances.
+
+---
+
+## 🚀 Getting Started
+
+### 1. Run the Dev Server
+Navigate to this directory and boot Vite:
+```bash
+npm run dev
+```
+
+### 2. Authentication
+Log in with either:
+- **System Admin / Staff Credentials**: email/password credentials.
+- **Customer Mobile Credentials**: 10-digit mobile number and 4-digit PIN.
+
+*Credentials matches your core Tillo application database.*
+
+---
+
+## 📡 API Reference Directory
+
+Include the custom header `X-Developer-Key: DEV-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` in all requests to query:
+
+| Method | Path | Description | Access Level |
+| :--- | :--- | :--- | :--- |
+| `GET` | `/api/developer/v1/validate` | Verify API key health and view owner details. | Key validated |
+| `GET` | `/api/developer/v1/stalls` | Fetch catalog listings of active canteen stalls. | Key validated |
+| `GET` | `/api/developer/v1/products` | Retrieve catalog list of products and current stock. | Key validated |
+| `GET` | `/api/developer/v1/wallet` | Fetch wallet balance (owner only) or total circulation volume. | Owner scope |
+| `GET` | `/api/developer/v1/orders` | Retrieve list of active orders (owner only) or order statuses. | Owner scope |
+
+---
+
+*Developed by the Canteen Automation Team.*
diff --git a/api-dashboard/eslint.config.js b/api-dashboard/eslint.config.js
new file mode 100644
index 00000000..ef614d25
--- /dev/null
+++ b/api-dashboard/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/api-dashboard/index.html b/api-dashboard/index.html
new file mode 100644
index 00000000..71905655
--- /dev/null
+++ b/api-dashboard/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ api-dashboard
+
+
+
+
+
+
diff --git a/api-dashboard/package-lock.json b/api-dashboard/package-lock.json
new file mode 100644
index 00000000..350f0165
--- /dev/null
+++ b/api-dashboard/package-lock.json
@@ -0,0 +1,2771 @@
+{
+ "name": "api-dashboard",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "api-dashboard",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "eslint": "^10.6.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "globals": "^17.7.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.62.0",
+ "vite": "^8.1.1"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
+ "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/type-utils": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.65.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
+ "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
+ "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.65.0",
+ "@typescript-eslint/types": "^8.65.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
+ "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
+ "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
+ "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
+ "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
+ "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.65.0",
+ "@typescript-eslint/tsconfig-utils": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/visitor-keys": "8.65.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
+ "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.65.0",
+ "@typescript-eslint/types": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
+ "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.65.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
+ "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
+ "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+ "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.44",
+ "caniuse-lite": "^1.0.30001806",
+ "electron-to-chromium": "^1.5.393",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.396",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
+ "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
+ "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz",
+ "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
+ "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
+ "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
+ "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.65.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
+ "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.65.0",
+ "@typescript-eslint/parser": "8.65.0",
+ "@typescript-eslint/typescript-estree": "8.65.0",
+ "@typescript-eslint/utils": "8.65.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.17",
+ "rolldown": "~1.1.5",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/api-dashboard/package.json b/api-dashboard/package.json
new file mode 100644
index 00000000..f6b24818
--- /dev/null
+++ b/api-dashboard/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "api-dashboard",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "eslint": "^10.6.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "globals": "^17.7.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.62.0",
+ "vite": "^8.1.1"
+ }
+}
diff --git a/api-dashboard/public/favicon.svg b/api-dashboard/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/api-dashboard/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api-dashboard/public/icons.svg b/api-dashboard/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/api-dashboard/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/api-dashboard/src/App.css b/api-dashboard/src/App.css
new file mode 100644
index 00000000..420f30e4
--- /dev/null
+++ b/api-dashboard/src/App.css
@@ -0,0 +1,1240 @@
+/* Main Container & Viewport Layouts */
+.api-dashboard-container.login-mode {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ background-color: var(--color-bg-main);
+ padding: 1rem;
+}
+
+.api-dashboard-layout {
+ display: flex;
+ min-height: 100vh;
+ background-color: var(--color-bg-main);
+}
+
+/* Glassmorphism & Cards */
+.glass-effect {
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid rgba(255, 255, 255, 0.5);
+ box-shadow: var(--shadow-md);
+}
+
+.premium-card {
+ background: var(--color-card-bg);
+ border-radius: 1.5rem;
+ border: 1px solid var(--color-border);
+ padding: 2rem;
+ box-shadow: var(--shadow-sm);
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.premium-card:hover {
+ box-shadow: var(--shadow-xl);
+ transform: translateY(-2px);
+}
+
+/* Header styling */
+.dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.25rem 2rem;
+ border-radius: 1.5rem;
+ border: 1px solid var(--color-border);
+ box-shadow: var(--shadow-sm);
+}
+
+.header-brand {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.tillo-logo-img {
+ height: 56px;
+ width: auto;
+ object-fit: contain;
+}
+
+.brand-texts h1 {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+ line-height: 1.2;
+}
+
+.brand-texts .subtitle {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--color-primary);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+/* User Badge */
+.user-profile-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+ background: rgba(0, 24, 40, 0.03);
+ padding: 0.5rem 0.5rem 0.5rem 1rem;
+ border-radius: 1rem;
+ border: 1px solid var(--color-border-light);
+}
+
+.user-profile-badge .avatar {
+ background: var(--color-primary);
+ color: white;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 0.9rem;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-info .username {
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: var(--color-text-dark);
+ line-height: 1.2;
+}
+
+.user-info .user-role {
+ font-size: 0.725rem;
+ color: var(--color-text-secondary);
+ font-weight: 500;
+}
+
+.btn-logout {
+ background: transparent;
+ border: none;
+ color: var(--color-text-muted);
+ width: 32px;
+ height: 32px;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-logout:hover {
+ background: rgba(239, 68, 68, 0.08);
+ color: var(--color-danger);
+}
+
+/* Toast Messages */
+.toast {
+ position: fixed;
+ top: 1.5rem;
+ right: 2rem;
+ padding: 1rem 1.5rem;
+ border-radius: 1rem;
+ color: white;
+ font-weight: 600;
+ font-size: 0.875rem;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+}
+
+@keyframes slideIn {
+ from { transform: translateX(100%) translateY(-10px); opacity: 0; }
+ to { transform: translateX(0) translateY(0); opacity: 1; }
+}
+
+.toast-success {
+ background: #08a850;
+ border: 1px solid #068d42;
+}
+
+.toast-error {
+ background: #ef4444;
+ border: 1px solid #dc2626;
+}
+
+/* Login Screen */
+.login-screen-wrapper {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem 0;
+}
+
+.login-card {
+ width: 100%;
+ max-width: 480px;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.login-card h2 {
+ font-size: 1.75rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+ text-align: center;
+}
+
+.login-desc {
+ color: var(--color-text-secondary);
+ font-size: 0.925rem;
+ text-align: center;
+ margin-top: -0.75rem;
+}
+
+.login-type-switch {
+ display: flex;
+ background: rgba(0, 24, 40, 0.04);
+ padding: 0.25rem;
+ border-radius: 0.85rem;
+}
+
+.login-type-switch button {
+ flex: 1;
+ background: transparent;
+ border: none;
+ padding: 0.75rem;
+ border-radius: 0.65rem;
+ font-weight: 700;
+ font-size: 0.875rem;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.login-type-switch button.active {
+ background: white;
+ color: var(--color-text-dark);
+ box-shadow: var(--shadow-sm);
+}
+
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+/* Forms & Inputs */
+.input-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.input-group label {
+ font-size: 0.825rem;
+ font-weight: 700;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.input-group input,
+.input-group select {
+ font-family: var(--font-sans);
+ font-size: 0.95rem;
+ padding: 0.85rem 1.1rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--color-border);
+ background-color: #fafdfb;
+ color: var(--color-text-primary);
+ outline: none;
+ transition: all 0.2s;
+ width: 100%;
+}
+
+.input-group input:focus,
+.input-group select:focus {
+ border-color: var(--color-primary);
+ background-color: white;
+ box-shadow: 0 0 0 3px var(--color-primary-glow);
+}
+
+.input-group select {
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23475569'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 1.1rem center;
+ background-size: 1.1rem;
+ padding-right: 2.5rem;
+}
+
+/* Buttons */
+.btn-primary {
+ background: var(--color-primary);
+ color: white;
+ font-weight: 700;
+ font-size: 0.95rem;
+ padding: 0.95rem 1.5rem;
+ border-radius: 1rem;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 16px -4px rgba(8, 168, 80, 0.25);
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--color-primary-dark);
+ box-shadow: 0 10px 20px -4px rgba(8, 168, 80, 0.35);
+ transform: translateY(-1px);
+}
+
+.btn-primary:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.btn-primary:disabled {
+ background: var(--color-text-muted);
+ box-shadow: none;
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
+.btn-postman {
+ background: #ff6c37;
+ color: white;
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 0.875rem;
+ padding: 0.85rem 1.25rem;
+ border-radius: 1rem;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 16px -4px rgba(255, 108, 55, 0.25);
+}
+
+.btn-postman:hover {
+ background: #ef5b25;
+ box-shadow: 0 10px 20px -4px rgba(255, 108, 55, 0.35);
+ transform: translateY(-1px);
+}
+
+.btn-postman:active {
+ transform: translateY(0);
+}
+
+.w-full {
+ width: 100%;
+}
+
+.demo-credentials-box {
+ background: rgba(8, 168, 80, 0.05);
+ border: 1px dashed rgba(8, 168, 80, 0.2);
+ border-radius: 1rem;
+ padding: 1.1rem;
+ margin-top: 0.5rem;
+}
+
+.demo-credentials-box h4 {
+ font-size: 0.85rem;
+ font-weight: 800;
+ color: var(--color-primary-dark);
+ margin-bottom: 0.25rem;
+}
+
+.demo-credentials-box p {
+ font-size: 0.8rem;
+ color: var(--color-text-secondary);
+}
+
+/* Main Dashboard Workspace Layout */
+.dashboard-sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 280px;
+ height: 100vh;
+ border-radius: 0;
+ border: none;
+ border-right: 1px solid var(--color-border);
+ padding: 2.25rem 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ z-index: 50;
+ background-color: var(--color-bg-sidebar);
+}
+
+.sidebar-brand {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.sidebar-footer {
+ margin-top: auto;
+ padding-top: 1rem;
+ border-top: 1px solid var(--color-border-light);
+}
+
+.sidebar-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ width: 100%;
+ padding: 0.85rem 1.1rem;
+ border-radius: 1rem;
+ border: none;
+ background: transparent;
+ color: var(--color-text-secondary);
+ font-weight: 700;
+ font-size: 0.875rem;
+ cursor: pointer;
+ text-align: left;
+ transition: all 0.2s;
+}
+
+.nav-item:hover {
+ background: rgba(0, 24, 40, 0.03);
+ color: var(--color-text-dark);
+}
+
+.nav-item.active {
+ background: var(--color-primary-glow);
+ color: var(--color-primary-dark);
+}
+
+.nav-icon {
+ flex-shrink: 0;
+}
+
+.nav-item-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.submenu {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ padding-left: 0.75rem;
+ margin-top: 0.25rem;
+ border-left: 2px solid var(--color-border);
+ margin-left: 1.1rem;
+}
+
+.submenu-item {
+ background: transparent;
+ border: none;
+ color: var(--color-text-secondary);
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: 0.775rem;
+ padding: 0.45rem 0.75rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ text-align: left;
+ transition: all 0.2s;
+ width: auto;
+}
+
+.submenu-item:hover {
+ background: rgba(0, 24, 40, 0.03);
+ color: var(--color-text-dark);
+}
+
+.security-guarantee-box {
+ background: var(--color-primary-glow);
+ border: 1px solid rgba(8, 168, 80, 0.1);
+ border-radius: 1.25rem;
+ padding: 1.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.shield-icon {
+ margin-bottom: 0.25rem;
+}
+
+.security-guarantee-box h4 {
+ font-size: 0.875rem;
+ font-weight: 800;
+ color: var(--color-primary-dark);
+}
+
+.security-guarantee-box p {
+ font-size: 0.775rem;
+ color: var(--color-text-secondary);
+ line-height: 1.4;
+}
+
+/* Main Content Area */
+.dashboard-main-content {
+ display: flex;
+ flex-direction: column;
+ margin-left: 280px;
+ flex: 1;
+ padding: 3rem;
+ min-height: 100vh;
+ overflow-y: auto;
+}
+
+.tab-pane {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.panel-header h2 {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.panel-header p {
+ color: var(--color-text-secondary);
+ font-size: 0.95rem;
+}
+
+/* Key Management Grid */
+.keys-grid {
+ display: grid;
+ grid-template-columns: 360px 1fr;
+ gap: 2rem;
+ align-items: start;
+}
+
+@media (max-width: 1200px) {
+ .keys-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.key-creator-card {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.key-creator-card h3 {
+ font-size: 1.15rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.card-desc {
+ font-size: 0.85rem;
+ color: var(--color-text-secondary);
+ margin-top: -0.75rem;
+}
+
+.create-key-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.limit-indicator {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.8rem;
+ color: var(--color-text-secondary);
+ background: rgba(0, 24, 40, 0.03);
+ padding: 0.6rem 1rem;
+ border-radius: 0.75rem;
+ align-self: flex-start;
+}
+
+.limit-indicator .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+.keys-list-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 4rem 2rem;
+ border: 2px dashed var(--color-border);
+ border-radius: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.empty-icon {
+ font-size: 2.5rem;
+}
+
+.empty-state h3 {
+ font-size: 1.25rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.empty-state p {
+ font-size: 0.9rem;
+ color: var(--color-text-secondary);
+ max-width: 320px;
+}
+
+.api-key-card {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.key-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+}
+
+.key-card-header h4 {
+ font-size: 1.1rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.badge-user-type {
+ font-size: 0.7rem;
+ font-weight: 800;
+ color: var(--color-primary-dark);
+ background: var(--color-primary-glow);
+ padding: 0.2rem 0.5rem;
+ border-radius: 0.5rem;
+ display: inline-block;
+ margin-top: 0.25rem;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+}
+
+.btn-revoke {
+ background: var(--color-danger-glow);
+ color: var(--color-danger);
+ border: 1px solid rgba(239, 68, 68, 0.15);
+ font-weight: 700;
+ font-size: 0.775rem;
+ padding: 0.4rem 0.85rem;
+ border-radius: 0.6rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-revoke:hover {
+ background: var(--color-danger);
+ color: white;
+}
+
+.key-display-box {
+ display: flex;
+ background: #fafdfb;
+ border: 1px solid var(--color-border);
+ border-radius: 0.85rem;
+ padding: 0.75rem 1rem;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.key-code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.875rem;
+ color: var(--color-text-primary);
+ font-weight: 600;
+ word-break: break-all;
+}
+
+.key-actions {
+ display: flex;
+ gap: 0.35rem;
+ flex-shrink: 0;
+}
+
+.btn-icon-only {
+ background: transparent;
+ border: none;
+ color: var(--color-text-secondary);
+ width: 32px;
+ height: 32px;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-icon-only:hover {
+ background: rgba(0, 24, 40, 0.04);
+ color: var(--color-text-dark);
+}
+
+.key-metadata {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.775rem;
+ color: var(--color-text-secondary);
+ border-top: 1px solid var(--color-border-light);
+ padding-top: 0.85rem;
+}
+
+/* Sandbox Playground Workspace */
+.sandbox-workspace {
+ display: grid;
+ grid-template-columns: 380px 1fr;
+ gap: 2rem;
+ align-items: start;
+}
+
+@media (max-width: 1100px) {
+ .sandbox-workspace {
+ grid-template-columns: 1fr;
+ }
+}
+
+.sandbox-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.security-audit-card {
+ border-color: rgba(234, 88, 12, 0.2);
+}
+
+.audit-card-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.audit-card-title h3 {
+ font-size: 1.15rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.attack-buttons-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-top: 1rem;
+}
+
+.btn-attack {
+ background: #fafdfb;
+ border: 1px solid rgba(234, 88, 12, 0.2);
+ color: var(--color-warning);
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 0.825rem;
+ padding: 0.75rem 1rem;
+ border-radius: 0.85rem;
+ cursor: pointer;
+ text-align: left;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.btn-attack:hover {
+ background: var(--color-warning-glow);
+ border-color: var(--color-warning);
+}
+
+.btn-attack::after {
+ content: "→";
+ font-size: 1.1rem;
+ font-weight: 900;
+}
+
+/* Sandbox Outputs Terminal */
+.sandbox-outputs {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.output-card {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.output-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--color-border-light);
+ padding-bottom: 0.85rem;
+}
+
+.output-card-header h3 {
+ font-size: 1.1rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.status-badge {
+ font-size: 0.775rem;
+ font-weight: 800;
+ padding: 0.35rem 0.75rem;
+ border-radius: 2rem;
+ letter-spacing: 0.02em;
+}
+
+.status-badge.success {
+ background: var(--color-primary-glow);
+ color: var(--color-primary-dark);
+ border: 1px solid rgba(8, 168, 80, 0.15);
+}
+
+.status-badge.error {
+ background: var(--color-danger-glow);
+ color: var(--color-danger);
+ border: 1px solid rgba(239, 68, 68, 0.15);
+}
+
+.status-badge.block {
+ background: var(--color-danger-glow);
+ color: var(--color-danger);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+}
+
+.status-badge.breach {
+ background: #fef3c7;
+ color: #d97706;
+ border: 1px solid #fcd34d;
+}
+
+.terminal-body {
+ background: #011627;
+ border-radius: 1rem;
+ padding: 1.5rem;
+ min-height: 240px;
+ max-height: 480px;
+ overflow: auto;
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.25);
+ display: flex;
+}
+
+.json-output {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.8rem;
+ color: #addb67; /* beautiful neonish green */
+ white-space: pre-wrap;
+ word-break: break-all;
+ width: 100%;
+}
+
+.terminal-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ color: var(--color-text-muted);
+ font-weight: 500;
+ font-size: 0.9rem;
+}
+
+.terminal-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ gap: 1rem;
+ color: #8093f1;
+}
+
+.spinner {
+ width: 24px;
+ height: 24px;
+ border: 3px solid rgba(128, 147, 241, 0.2);
+ border-top-color: #8093f1;
+ border-radius: 50%;
+ animation: loading-spin 0.8s linear infinite;
+}
+
+@keyframes loading-spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Attack terminal custom designs */
+.attack-output-card {
+ border-color: rgba(234, 88, 12, 0.25);
+}
+
+.text-orange {
+ color: var(--color-warning);
+}
+
+.attack-terminal {
+ background: #110702;
+ border: 1px solid rgba(234, 88, 12, 0.15);
+}
+
+.orange-spinner {
+ border-color: rgba(234, 88, 12, 0.2);
+ border-top-color: var(--color-warning);
+}
+
+.firewall-audit-header {
+ font-family: ui-monospace, monospace;
+ font-size: 0.85rem;
+ color: #ffffff;
+ padding-bottom: 0.85rem;
+ margin-bottom: 0.85rem;
+ border-bottom: 1px dashed rgba(234, 88, 12, 0.2);
+}
+
+.green-text {
+ color: #10b981;
+ font-weight: 800;
+}
+
+.json-output.attack-output {
+ color: #f87171; /* soft red */
+}
+
+/* Documentation Reference View */
+.docs-card {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.docs-card h3 {
+ font-size: 1.25rem;
+ font-weight: 800;
+ color: var(--color-text-dark);
+}
+
+.docs-card h3.section-title {
+ margin-top: 1rem;
+ border-top: 1px solid var(--color-border);
+ padding-top: 1.5rem;
+}
+
+.code-block-wrapper {
+ background: #0f172a;
+ border-radius: 1rem;
+ padding: 1.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.code-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: var(--color-text-muted);
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.btn-copy-code {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ border: none;
+ font-weight: 700;
+ font-size: 0.7rem;
+ padding: 0.35rem 0.75rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-copy-code:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.code-pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.85rem;
+ color: #38bdf8; /* light sky blue */
+ margin: 0;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.docs-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 0.5rem;
+}
+
+.docs-table th,
+.docs-table td {
+ padding: 1rem;
+ text-align: left;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.docs-table th {
+ font-weight: 800;
+ color: var(--color-text-dark);
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+ background: rgba(0, 24, 40, 0.02);
+}
+
+.docs-table td {
+ font-size: 0.875rem;
+ vertical-align: top;
+}
+
+.method-get {
+ background: rgba(16, 185, 129, 0.1);
+ color: #059669;
+ font-weight: 800;
+ font-size: 0.725rem;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.4rem;
+ display: inline-block;
+}
+
+.security-notice-banner {
+ background: var(--color-primary-glow);
+ border: 1px solid rgba(8, 168, 80, 0.15);
+ border-radius: 1.25rem;
+ padding: 1.5rem;
+ margin-top: 1rem;
+}
+
+.security-notice-banner h4 {
+ font-size: 1rem;
+ font-weight: 800;
+ color: var(--color-primary-dark);
+ margin-bottom: 0.5rem;
+}
+
+.security-notice-banner p {
+ font-size: 0.85rem;
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+}
+
+/* Elevated permissions toggles and scopes layout */
+.permissions-config {
+ margin-top: 1.5rem;
+ padding-top: 1.25rem;
+ border-top: 1px solid var(--color-border);
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.toggle-label-header {
+ font-size: 0.775rem;
+ font-weight: 800;
+ color: var(--color-text-secondary);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ margin-bottom: 0.25rem;
+ display: block;
+}
+
+.toggle-switch-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.75rem;
+}
+
+/* Sliding switch container */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 44px;
+ height: 24px;
+ flex-shrink: 0;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #cbd5e1;
+ transition: .3s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ transition: .3s;
+}
+
+input:checked + .slider {
+ background-color: #ef6c37;
+}
+
+input:checked + .slider:before {
+ transform: translateX(20px);
+}
+
+.slider.round {
+ border-radius: 24px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
+
+.toggle-desc {
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--color-text-dark);
+}
+
+.permissions-checklist {
+ display: flex;
+ flex-direction: column;
+ gap: 0.65rem;
+ margin-bottom: 1rem;
+}
+
+.permission-group-card {
+ background: rgba(0, 24, 40, 0.015);
+ border: 1px solid var(--color-border-light);
+ border-radius: 0.75rem;
+ padding: 0.65rem 0.85rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+}
+
+.permission-group-card h4 {
+ font-size: 0.825rem;
+ font-weight: 700;
+ color: var(--color-text-dark);
+ margin: 0;
+}
+
+.permission-checkboxes {
+ display: flex;
+ gap: 1rem;
+}
+
+.checkbox-container {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ user-select: none;
+}
+
+.checkbox-container input {
+ cursor: pointer;
+}
+
+.checkbox-container.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+/* API Key Card Scopes display */
+.badge-access-type {
+ font-size: 0.65rem;
+ font-weight: 800;
+ padding: 0.15rem 0.4rem;
+ border-radius: 0.25rem;
+ text-transform: uppercase;
+}
+
+.badge-access-type.elevated {
+ background: rgba(239, 108, 55, 0.1);
+ color: #ef6c37;
+}
+
+.badge-access-type.readonly {
+ background: rgba(8, 168, 80, 0.1);
+ color: #08a850;
+}
+
+.key-permissions-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ margin-top: 0.75rem;
+ border-top: 1px dashed var(--color-border);
+ padding-top: 0.75rem;
+}
+
+.scopes-label {
+ font-size: 0.725rem;
+ font-weight: 800;
+ color: var(--color-text-secondary);
+}
+
+.scopes-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.scope-tag {
+ font-size: 0.625rem;
+ font-weight: 700;
+ background: rgba(0, 24, 40, 0.04);
+ color: var(--color-text-dark);
+ padding: 0.1rem 0.4rem;
+ border-radius: 0.25rem;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+}
diff --git a/api-dashboard/src/App.tsx b/api-dashboard/src/App.tsx
new file mode 100644
index 00000000..21ba5101
--- /dev/null
+++ b/api-dashboard/src/App.tsx
@@ -0,0 +1,1266 @@
+import React, { useState, useEffect } from 'react';
+import logo from './assets/logo.png';
+import './App.css';
+
+interface DeveloperApiKey {
+ id: number;
+ apiKey: string;
+ name: string;
+ userId: number;
+ userType: string;
+ ownerIdentifier: string;
+ active: boolean;
+ createdAt: string;
+ lastUsedAt?: string;
+ writeAccess?: boolean;
+ permissions?: string;
+}
+
+interface UserSession {
+ token: string;
+ id: number;
+ name: string;
+ emailOrMobile: string;
+ roleOrType: string;
+}
+
+const BACKEND_URL = window.location.origin.includes('localhost')
+ ? 'http://localhost:8080'
+ : window.location.origin;
+
+function App() {
+ // Auth State
+ const [session, setSession] = useState(null);
+ const [loginType, setLoginType] = useState<'CUSTOMER' | 'SYSTEM'>('SYSTEM');
+ const [loginMobile, setLoginMobile] = useState('');
+ const [loginPin, setLoginPin] = useState('');
+ const [loginEmail, setLoginEmail] = useState('');
+ const [loginPassword, setLoginPassword] = useState('');
+
+ // Key Management State
+ const [keys, setKeys] = useState([]);
+ const [newKeyName, setNewKeyName] = useState('');
+ const [showSecrets, setShowSecrets] = useState>({});
+ const [writeAccess, setWriteAccess] = useState(false);
+ const [selectedPermissions, setSelectedPermissions] = useState([
+ 'READ_PRODUCTS', 'READ_STALLS', 'READ_ORDERS', 'READ_WALLETS'
+ ]);
+
+ // UI Tabs & Feedback
+ const [activeTab, setActiveTab] = useState<'keys' | 'playground' | 'docs'>('keys');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(null);
+
+ // Playground Sandbox State
+ const [selectedKey, setSelectedKey] = useState('');
+ const [selectedEndpoint, setSelectedEndpoint] = useState('/api/developer/v1/validate');
+ const [customKeyInput, setCustomKeyInput] = useState('');
+ const [playgroundLoading, setPlaygroundLoading] = useState(false);
+ const [playgroundStatus, setPlaygroundStatus] = useState(null);
+ const [playgroundResponse, setPlaygroundResponse] = useState(null);
+
+ // Simulated Attack State
+ const [attackLoading, setAttackLoading] = useState(false);
+ const [attackStatus, setAttackStatus] = useState(null);
+ const [attackResponse, setAttackResponse] = useState(null);
+
+ // Restore session from localStorage on mount
+ useEffect(() => {
+ const saved = localStorage.getItem('developer_session');
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ setSession(parsed);
+ } catch (e) {
+ localStorage.removeItem('developer_session');
+ }
+ }
+ }, []);
+
+ // Fetch keys when session changes
+ useEffect(() => {
+ if (session) {
+ fetchKeys();
+ } else {
+ setKeys([]);
+ }
+ }, [session]);
+
+ const clearMessages = () => {
+ setError(null);
+ setSuccess(null);
+ };
+
+ const showNotification = (type: 'success' | 'error', msg: string) => {
+ if (type === 'success') {
+ setSuccess(msg);
+ setTimeout(() => setSuccess(null), 4000);
+ } else {
+ setError(msg);
+ setTimeout(() => setError(null), 5000);
+ }
+ };
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ clearMessages();
+
+ try {
+ if (loginType === 'SYSTEM') {
+ const res = await fetch(`${BACKEND_URL}/api/system/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: loginEmail, password: loginPassword }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'System Login failed');
+
+ const sessionData: UserSession = {
+ token: data.token,
+ id: data.id,
+ name: data.name,
+ emailOrMobile: data.email,
+ roleOrType: data.role,
+ };
+
+ setSession(sessionData);
+ localStorage.setItem('developer_session', JSON.stringify(sessionData));
+ showNotification('success', `Welcome back, ${data.name}!`);
+ } else {
+ // Customer login
+ const res = await fetch(`${BACKEND_URL}/api/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ mobileNumber: loginMobile, pin: loginPin }),
+ });
+
+ const data = await res.json();
+ if (!res.ok || !data.success) throw new Error(data.message || 'Customer Login failed');
+
+ const sessionData: UserSession = {
+ token: data.token,
+ id: data.user.id,
+ name: data.user.name || 'Customer',
+ emailOrMobile: data.user.mobileNumber,
+ roleOrType: 'CUSTOMER',
+ };
+
+ setSession(sessionData);
+ localStorage.setItem('developer_session', JSON.stringify(sessionData));
+ showNotification('success', `Welcome back, ${sessionData.name}!`);
+ }
+ } catch (err: any) {
+ showNotification('error', err.message || 'An error occurred during authentication');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleLogout = () => {
+ localStorage.removeItem('developer_session');
+ setSession(null);
+ clearMessages();
+ showNotification('success', 'Logged out successfully');
+ };
+
+ const fetchKeys = async () => {
+ if (!session) return;
+ try {
+ const res = await fetch(`${BACKEND_URL}/api/developer-keys`, {
+ headers: {
+ 'Authorization': `Bearer ${session.token}`,
+ },
+ });
+ if (!res.ok) throw new Error('Failed to fetch API keys');
+ const data = await res.json();
+ setKeys(data);
+ if (data.length > 0 && !selectedKey) {
+ setSelectedKey(data[0].apiKey);
+ }
+ } catch (err: any) {
+ showNotification('error', err.message);
+ }
+ };
+
+ const handleCreateKey = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!session) return;
+ if (!newKeyName.trim()) {
+ showNotification('error', 'Please enter a name for the API key');
+ return;
+ }
+ setLoading(true);
+ clearMessages();
+
+ try {
+ const isSystemUser = session.roleOrType === 'MASTER' || session.roleOrType === 'MANAGER';
+ const permissionsPayload = isSystemUser
+ ? selectedPermissions.join(',')
+ : 'READ_PRODUCTS,READ_STALLS,READ_ORDERS,READ_WALLETS';
+ const writeAccessPayload = isSystemUser ? writeAccess : false;
+
+ const res = await fetch(`${BACKEND_URL}/api/developer-keys`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session.token}`,
+ },
+ body: JSON.stringify({
+ name: newKeyName,
+ writeAccess: writeAccessPayload,
+ permissions: permissionsPayload
+ }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'Failed to generate key');
+
+ showNotification('success', 'API Key generated successfully');
+ setNewKeyName('');
+ fetchKeys();
+ } catch (err: any) {
+ showNotification('error', err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRevokeKey = async (id: number) => {
+ if (!session) return;
+ if (!window.confirm('Are you sure you want to revoke this API key? Applications using this key will immediately lose access.')) return;
+ setLoading(true);
+ clearMessages();
+
+ try {
+ const res = await fetch(`${BACKEND_URL}/api/developer-keys/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${session.token}`,
+ },
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || 'Failed to revoke key');
+
+ showNotification('success', 'API Key revoked and deleted');
+ fetchKeys();
+ } catch (err: any) {
+ showNotification('error', err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ showNotification('success', 'Copied to clipboard!');
+ };
+
+ const navigateToSection = (sectionId: string) => {
+ setActiveTab('docs');
+ setTimeout(() => {
+ const element = document.getElementById(sectionId);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ }, 100);
+ };
+
+ const downloadPostmanCollection = () => {
+ const keyToUse = customKeyInput || selectedKey || 'YOUR_API_KEY_HERE';
+ const collectionName = 'Tillo Developer API';
+
+ const postmanJson = {
+ info: {
+ name: collectionName,
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
+ description: 'Postman Collection generated dynamically from Tillo Developer Console to test Developer APIs.'
+ },
+ variable: [
+ {
+ key: 'baseUrl',
+ value: BACKEND_URL,
+ type: 'string'
+ },
+ {
+ key: 'apiKey',
+ value: keyToUse,
+ type: 'string'
+ }
+ ],
+ item: [
+ {
+ name: 'Health Check / Validate Key',
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/validate',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'validate']
+ }
+ }
+ },
+ {
+ name: 'Get Active Stalls',
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/stalls',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'stalls']
+ }
+ }
+ },
+ {
+ name: 'Get Active Products',
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/products',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'products']
+ }
+ }
+ },
+ {
+ name: 'Get Wallet Balance (Own)',
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/wallet',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'wallet']
+ }
+ }
+ },
+ {
+ name: "Query User's Wallet Balance (By Mobile)",
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/wallet?mobileNumber=9876543210',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'wallet'],
+ query: [
+ {
+ key: 'mobileNumber',
+ value: '9876543210'
+ }
+ ]
+ }
+ }
+ },
+ {
+ name: 'Get Active Orders',
+ request: {
+ method: 'GET',
+ header: [
+ {
+ key: 'X-Developer-Key',
+ value: '{{apiKey}}',
+ type: 'text'
+ }
+ ],
+ url: {
+ raw: '{{baseUrl}}/api/developer/v1/orders',
+ host: ['{{baseUrl}}'],
+ path: ['api', 'developer', 'v1', 'orders']
+ }
+ }
+ }
+ ]
+ };
+
+ const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(postmanJson, null, 2));
+ const downloadAnchor = document.createElement('a');
+ downloadAnchor.setAttribute('href', dataStr);
+ downloadAnchor.setAttribute('download', 'Tillo_Developer_API.postman_collection.json');
+ document.body.appendChild(downloadAnchor);
+ downloadAnchor.click();
+ downloadAnchor.remove();
+ showNotification('success', 'Postman Collection downloaded! Import it directly into Postman to start testing.');
+ };
+
+ const toggleRevealKey = (id: number) => {
+ setShowSecrets(prev => ({ ...prev, [id]: !prev[id] }));
+ };
+
+ const executePlayground = async () => {
+ const apiKeyToUse = customKeyInput || selectedKey;
+ if (!apiKeyToUse) {
+ showNotification('error', 'Please select or enter a developer API Key');
+ return;
+ }
+
+ setPlaygroundLoading(true);
+ setPlaygroundStatus(null);
+ setPlaygroundResponse(null);
+
+ try {
+ const res = await fetch(`${BACKEND_URL}${selectedEndpoint}`, {
+ headers: {
+ 'X-Developer-Key': apiKeyToUse,
+ },
+ });
+ setPlaygroundStatus(res.status);
+ const data = await res.json();
+ setPlaygroundResponse(data);
+ } catch (err: any) {
+ setPlaygroundResponse({ error: err.message || 'Network request failed' });
+ } finally {
+ setPlaygroundLoading(false);
+ }
+ };
+
+ const runSimulatedAttack = async (attackUrl: string, method: string = 'GET') => {
+ const apiKeyToUse = customKeyInput || selectedKey;
+ if (!apiKeyToUse) {
+ showNotification('error', 'Please select or enter an API Key to simulate the attack');
+ return;
+ }
+
+ setAttackLoading(true);
+ setAttackStatus(null);
+ setAttackResponse(null);
+
+ try {
+ const options: RequestInit = {
+ method,
+ headers: {
+ 'X-Developer-Key': apiKeyToUse,
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ if (method === 'POST') {
+ options.body = JSON.stringify({ amount: '1000', userId: 1 });
+ }
+
+ const res = await fetch(`${BACKEND_URL}${attackUrl}`, options);
+ setAttackStatus(res.status);
+ const data = await res.json();
+ setAttackResponse(data);
+ } catch (err: any) {
+ setAttackResponse({ error: err.message || 'Request blocked or failed' });
+ } finally {
+ setAttackLoading(false);
+ }
+ };
+
+ const isSystemUser = session ? (session.roleOrType === 'MASTER' || session.roleOrType === 'MANAGER') : false;
+ const isLimitReached = !isSystemUser && keys.length >= 3;
+
+ return (
+
+ {/* Notifications */}
+ {success &&
{success}
}
+ {error &&
{error}
}
+
+ {!session ? (
+ /* Login Panel */
+
+
+
+
+
+
Access Developer Console
+
Use your existing Tillo user credentials or system administrator login to manage API keys.
+
+
+ { setLoginType('SYSTEM'); clearMessages(); }}
+ >
+ System Admin / Staff
+
+ { setLoginType('CUSTOMER'); clearMessages(); }}
+ >
+ Customer Portal
+
+
+
+
+
+
+
💡 Dev Sandbox Access:
+ {loginType === 'SYSTEM' ? (
+
Default Admin: admin / Password: admin123
+ ) : (
+
Create / use any customer login registered on the POS client. (e.g. mobile 9876543210 , pin 123456 )
+ )}
+
+
+
+ ) : (
+ <>
+ {/* Full Height left sidebar */}
+
+
+ {/* Main Content Area */}
+
+ {activeTab === 'keys' && (
+
+
+
Manage Developer API Keys
+
Obtain authentication tokens for integrating program modules and external query widgets.
+
+
+ {/* API Key list */}
+
+
+
Generate New API Key
+
Assign a friendly name to describe your application module usage.
+
+
+
+ {isSystemUser ? (
+
+
+ Unlimited Staff API Keys Allowed
+
+ ) : (
+
+ = 3 ? '#ef4444' : '#08a850' }}>
+ Keys generated: {keys.length} of 3 limit
+
+ )}
+
+
+
+ {keys.length === 0 ? (
+
+
🔐
+
No Active API Keys
+
You haven't generated any API keys yet. Create one on the left to start accessing endpoints.
+
+ ) : (
+ keys.map((key) => (
+
+
+
+
{key.name}
+
+ {key.userType} Key
+
+ {key.writeAccess ? 'Elevated (Write)' : 'Read-Only'}
+
+
+
+
handleRevokeKey(key.id)}
+ className="btn-revoke"
+ title="Revoke and Delete Key"
+ >
+ Revoke
+
+
+
+
+
+ {showSecrets[key.id] ? key.apiKey : `${key.apiKey.substring(0, 8)}••••••••••••••••••••••••••••`}
+
+
+
toggleRevealKey(key.id)}
+ className="btn-icon-only"
+ title={showSecrets[key.id] ? 'Hide Key' : 'Reveal Key'}
+ >
+ {showSecrets[key.id] ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+
+
copyToClipboard(key.apiKey)}
+ className="btn-icon-only"
+ title="Copy Key"
+ >
+
+
+
+
+
+
+
+
+ {key.permissions && (
+
+
Scopes:
+
+ {key.permissions.split(',').map(scope => (
+ {scope}
+ ))}
+
+
+ )}
+
+
+ Created: {new Date(key.createdAt).toLocaleDateString()}
+ Last Used: {key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleTimeString() : 'Never'}
+
+
+ ))
+ )}
+
+
+
+ )}
+
+ {activeTab === 'playground' && (
+
+
+
Developer Sandbox Playground
+
Query endpoints live and observe the robust read-only and anti-financial access controls.
+
+
+
+
+
+
Configure Sandbox Query
+
+
+ Select API Key
+ {
+ setSelectedKey(e.target.value);
+ setCustomKeyInput('');
+ }}
+ >
+ {keys.map((k) => (
+ {k.name} ({k.apiKey.substring(0, 8)}...)
+ ))}
+ -- Use Custom Key (Enter Below) --
+
+
+
+ {!selectedKey && (
+
+ Custom API Key
+ setCustomKeyInput(e.target.value)}
+ />
+
+ )}
+
+
+ Target Endpoint
+ setSelectedEndpoint(e.target.value)}
+ >
+ GET /api/developer/v1/validate (Validation check)
+ GET /api/developer/v1/stalls (Stalls Catalogue)
+ GET /api/developer/v1/products (Products Catalog)
+ GET /api/developer/v1/wallet (Own Wallet Balance)
+ GET /api/developer/v1/wallet?mobileNumber=9876543210 (Check Any Wallet by Mobile)
+ GET /api/developer/v1/orders (Active Orders list)
+
+
+
+
+ {playgroundLoading ? 'Sending Request...' : 'Send API Request'}
+
+
+
+
+
+
+ Test with Postman (Export Collection)
+
+
+
+ {/* Simulated security audit / attack tests */}
+
+
+
+
+
+
Simulate Security Audit Attacks
+
+
Try to breach financial or write layers using the developer key. Watch Tillo's robust guard mechanism reject it.
+
+
+ runSimulatedAttack('/api/wallet/transactions/all')}
+ className="btn-attack"
+ disabled={attackLoading}
+ >
+ Attack Ledger: Get All User Transactions
+
+
+ runSimulatedAttack('/api/wallet/topup', 'POST')}
+ className="btn-attack"
+ disabled={attackLoading}
+ >
+ Attack Write: Inject Wallet Topup
+
+
+ runSimulatedAttack('/api/wallet/stats')}
+ className="btn-attack"
+ disabled={attackLoading}
+ >
+ Attack Core: Fetch Global Financial Stats
+
+
+
+
+
+
+
+
+
Sandbox Terminal Response
+ {playgroundStatus !== null && (
+ = 200 && playgroundStatus < 300 ? 'success' : 'error'}`}>
+ HTTP {playgroundStatus}
+
+ )}
+
+
+
+ {playgroundLoading ? (
+
+
+
Request dispatched... waiting for backend frame
+
+ ) : playgroundResponse ? (
+
+ {JSON.stringify(playgroundResponse, null, 2)}
+
+ ) : (
+
+ Ready. Select an endpoint above and click "Send API Request".
+
+ )}
+
+
+
+ {/* Attack response audit terminal */}
+ {(attackResponse || attackLoading) && (
+
+
+
Audit Firewall Log
+ {attackStatus !== null && (
+ = 400 ? 'block' : 'breach'}`}>
+ {attackStatus === 401 || attackStatus === 403 ? 'BLOCKED - SECURE' : `STATUS ${attackStatus}`}
+
+ )}
+
+
+
+ {attackLoading ? (
+
+
+
Penetration query executing...
+
+ ) : (
+
+
+ [SHIELD-ACTIVE] Detected access violation on restricted resource. Access Denied.
+
+
+ {JSON.stringify(attackResponse, null, 2)}
+
+
+ )}
+
+
+ )}
+
+
+
+ )}
+
+ {activeTab === 'docs' && (
+
+
+
API Reference Integration Guide
+
How to use your Developer API keys to authenticate and query Tillo system details.
+
+
+
+
1. System Architecture & Scope Segregation
+
+ The Tillo Developer API acts as a read-only integration layer designed to support external widgets, active status screens, and profile integrations.
+ Authorization is separated based on the owner of the API Key:
+
+
+
+
+ Customer Key Scopes: Allows programmatic reading of the key owner's own orders and wallet balance. Perfect for building custom widgets or personal status trackers.
+ System Key Scopes: Allows canteen-wide catalog querying (stalls, product list, inventory levels) and overall statistics, but redacts specific customer transaction profiles.
+
+
+
+
2. Authentication & Header Specs
+
+ All endpoints require you to supply your custom key inside the X-Developer-Key HTTP header.
+ Traditional JWT authorization headers (e.g., Authorization: Bearer ...) are not required and will be ignored when querying developer paths.
+
+
+
+
+
HTTP Request Example
+
+ Test with Postman
+ copyToClipboard('curl -H "X-Developer-Key: DEV-YOUR_API_KEY_HERE" ' + BACKEND_URL + '/api/developer/v1/stalls')} className="btn-copy-code">Copy curl
+
+
+
+{`curl -X GET \\
+ -H "X-Developer-Key: DEV-YOUR_API_KEY_HERE" \\
+ ${BACKEND_URL}/api/developer/v1/stalls`}
+
+
+
+
3. Endpoint Directory (Read-Only)
+
+
+
+
+ HTTP Method
+ Path
+ Description / Usage
+ Permissions / Scope
+
+
+
+
+ GET
+ /api/developer/v1/validate
+ Verifies key health, active status, and returns key metadata.
+ Key validated
+
+
+ GET
+ /api/developer/v1/stalls
+ Lists active canteen stalls, descriptions, and operational status.
+ Key validated
+
+
+ GET
+ /api/developer/v1/products
+ Returns the active menu item catalog, categories, pricing, and current stock.
+ Key validated
+
+
+ GET
+ /api/developer/v1/wallet
+
+ Returns wallet balance.
+ • Default : Returns own balance (for Customer Key) or circulation stats (for System Key).
+ • Query Parameter : Pass ?mobileNumber=... or ?userId=... to check the wallet balance of any user in the system.
+
+ Any user balance, read-only
+
+
+ GET
+ /api/developer/v1/orders
+
+ Retrieves active and completed order lists.
+ • Customer Key : Returns key owner's own orders.
+ • System Key : Lists system orders with financial elements (payout details) masked.
+
+ Owner scope, key validated
+
+
+
+
+
4. Core Rules & Security Shields
+
+
🔒 Strict Mutation and Financial Block
+
+ To prevent unauthorized changes or leaks of financial parameters, developer API keys are bounded by strict policies:
+
+
+ Read-Only Constraints: Standard operations like checking out, creating coupon codes, placing orders, or topping up balances are blocked. Mutation endpoints will return 401 Unauthorized or 403 Forbidden.
+ Financial Isolation: General database tables containing financial audits, payment processor reference IDs, vendor billing settlements, and total daily canteen earnings are inaccessible to API keys.
+ Quota Limit: To prevent token spam, a hard limit of 3 API keys is enforced per account. Revoke an old key to create a new one.
+
+
+
+
5. Error Responses Schema
+
When query violations occur, the firewall returns standard JSON error bodies to simplify client parsing:
+
+
+ HTTP 401 Unauthorized / Invalid Key
+
+
+{`{
+ "error": "Invalid or missing X-Developer-Key header"
+}`}
+
+
+
+
+ HTTP 403 Forbidden / Access Denied
+
+
+{`{
+ "status": 403,
+ "error": "Forbidden",
+ "message": "Access Denied on restricted financial/mutation endpoint",
+ "path": "/api/wallet/topup"
+}`}
+
+
+
+
6. Quick Integration JavaScript Snippet
+
+
+ JavaScript Fetch Template
+
+
+{`async function getCanteenMenu(apiKey) {
+ try {
+ const response = await fetch('${BACKEND_URL}/api/developer/v1/products', {
+ headers: {
+ 'X-Developer-Key': apiKey
+ }
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.json();
+ throw new Error(errorBody.error || 'Failed to fetch');
+ }
+
+ const products = await response.json();
+ console.log('Active Products:', products);
+ return products;
+ } catch (error) {
+ console.error('API Error:', error.message);
+ }
+}`}
+
+
+
+
+ )}
+
+ >
+ )}
+
+ );
+}
+
+export default App;
diff --git a/api-dashboard/src/assets/Tillo.png b/api-dashboard/src/assets/Tillo.png
new file mode 100644
index 00000000..7bdd84ca
Binary files /dev/null and b/api-dashboard/src/assets/Tillo.png differ
diff --git a/api-dashboard/src/assets/hero.png b/api-dashboard/src/assets/hero.png
new file mode 100644
index 00000000..02251f4b
Binary files /dev/null and b/api-dashboard/src/assets/hero.png differ
diff --git a/api-dashboard/src/assets/logo.png b/api-dashboard/src/assets/logo.png
new file mode 100644
index 00000000..c4b79e3f
Binary files /dev/null and b/api-dashboard/src/assets/logo.png differ
diff --git a/api-dashboard/src/assets/react.svg b/api-dashboard/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/api-dashboard/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api-dashboard/src/assets/vite.svg b/api-dashboard/src/assets/vite.svg
new file mode 100644
index 00000000..5101b674
--- /dev/null
+++ b/api-dashboard/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/api-dashboard/src/index.css b/api-dashboard/src/index.css
new file mode 100644
index 00000000..dcada87e
--- /dev/null
+++ b/api-dashboard/src/index.css
@@ -0,0 +1,73 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
+
+:root {
+ --font-sans: "Inter", "system-ui", "-apple-system", "sans-serif";
+ --color-primary: #08a850;
+ --color-primary-light: #0cb859;
+ --color-primary-dark: #068d42;
+ --color-primary-glow: rgba(8, 168, 80, 0.06);
+ --color-bg-main: #f4fbf7;
+ --color-bg-sidebar: #ffffff;
+ --color-text-dark: #001828;
+ --color-text-primary: #0f172a;
+ --color-text-secondary: #475569;
+ --color-text-muted: #94a3b8;
+ --color-border: rgba(0, 24, 40, 0.08);
+ --color-border-light: rgba(0, 24, 40, 0.04);
+ --color-danger: #ef4444;
+ --color-danger-glow: rgba(239, 68, 68, 0.08);
+ --color-warning: #ea580c;
+ --color-warning-glow: rgba(234, 88, 12, 0.08);
+ --color-card-bg: #ffffff;
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
+ --shadow-xl: 0 20px 25px -5px rgba(8, 168, 80, 0.04), 0 10px 10px -5px rgba(8, 168, 80, 0.02);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ border-color: var(--color-border);
+}
+
+body {
+ font-family: var(--font-sans);
+ background-color: var(--color-bg-main);
+ color: var(--color-text-primary);
+ min-height: 100vh;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#root {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Custom Scrollbars */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-border) transparent;
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: var(--color-border);
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: var(--color-text-muted);
+}
diff --git a/api-dashboard/src/main.tsx b/api-dashboard/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/api-dashboard/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/api-dashboard/tsconfig.app.json b/api-dashboard/tsconfig.app.json
new file mode 100644
index 00000000..6830b6f7
--- /dev/null
+++ b/api-dashboard/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/api-dashboard/tsconfig.json b/api-dashboard/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/api-dashboard/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/api-dashboard/tsconfig.node.json b/api-dashboard/tsconfig.node.json
new file mode 100644
index 00000000..8455dcbc
--- /dev/null
+++ b/api-dashboard/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/api-dashboard/vite.config.ts b/api-dashboard/vite.config.ts
new file mode 100644
index 00000000..8b0f57b9
--- /dev/null
+++ b/api-dashboard/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java
index 637d631d..ce790d5f 100644
--- a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java
+++ b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java
@@ -82,6 +82,8 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
.requestMatchers(HttpMethod.POST, "/api/orders/*/cancel").authenticated()
+ .requestMatchers("/api/developer-keys/**").authenticated()
+ .requestMatchers("/api/developer/v1/**").permitAll()
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
// ── STAFF/MANAGER/MASTER: All other management APIs ──
diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiController.java b/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiController.java
new file mode 100644
index 00000000..a512fce2
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiController.java
@@ -0,0 +1,515 @@
+package com.rit.canteen.sales.controller;
+
+import com.rit.canteen.sales.model.*;
+import com.rit.canteen.sales.repository.*;
+import com.rit.canteen.sales.service.DeveloperApiKeyService;
+import com.rit.canteen.sales.service.TokenService;
+import com.rit.canteen.sales.service.SystemNotificationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/developer/v1")
+public class DeveloperApiController {
+
+ @Autowired
+ private DeveloperApiKeyService keyService;
+
+ @Autowired
+ private StallRepository stallRepository;
+
+ @Autowired
+ private ProductRepository productRepository;
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private TokenTransactionRepository transactionRepository;
+
+ @Autowired
+ private TokenService tokenService;
+
+ @Autowired
+ private SystemNotificationService notificationService;
+
+ @Autowired
+ private StockUpdateController stockUpdateController;
+
+ // ── Helper to authenticate key ─────────────────────────────────────────
+ private DeveloperApiKey authenticate(String headerKey) {
+ if (headerKey == null || headerKey.trim().isEmpty()) {
+ return null;
+ }
+ return keyService.validateAndUseKey(headerKey).orElse(null);
+ }
+
+ private boolean checkPermission(DeveloperApiKey key, String requiredScope) {
+ if (key == null) return false;
+
+ // If the operation requires write access but the key doesn't have it, block immediately.
+ if (requiredScope.startsWith("WRITE_") && !key.isWriteAccess()) {
+ return false;
+ }
+
+ // If it's a customer key, they only get default read-only scopes.
+ if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
+ return requiredScope.startsWith("READ_");
+ }
+
+ // System users get custom scopes.
+ String scopes = key.getPermissions();
+ if (scopes == null || scopes.trim().isEmpty()) {
+ return false;
+ }
+
+ return Arrays.stream(scopes.split(","))
+ .map(String::trim)
+ .anyMatch(scope -> scope.equalsIgnoreCase(requiredScope));
+ }
+
+ // ── 1. Validate API Key ────────────────────────────────────────────────
+ @GetMapping("/validate")
+ public ResponseEntity> validateKey(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ return ResponseEntity.ok(Map.of(
+ "status", "VALID",
+ "name", key.getName(),
+ "ownerType", key.getUserType(),
+ "createdAt", key.getCreatedAt(),
+ "readOnly", !key.isWriteAccess(),
+ "permissions", key.getPermissions() != null ? Arrays.asList(key.getPermissions().split(",")) : Collections.emptyList()
+ ));
+ }
+
+ // ── 2. Get Stalls (Read-only, non-financial) ───────────────────────────
+ @GetMapping("/stalls")
+ public ResponseEntity> getStalls(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "READ_STALLS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_STALLS permission required"));
+ }
+
+ List stalls = stallRepository.findAll();
+ // Map to DTO to avoid circular references and hide sensitive fields
+ List> result = stalls.stream().map(s -> {
+ Map map = new HashMap<>();
+ map.put("id", s.getId());
+ map.put("name", s.getName());
+ map.put("description", s.getDescription());
+ map.put("active", s.isActive());
+ map.put("temporarilyClosed", s.isTemporarilyClosed());
+ return map;
+ }).collect(Collectors.toList());
+
+ return ResponseEntity.ok(result);
+ }
+
+ // ── 3. Get Products (Read-only, non-financial) ─────────────────────────
+ @GetMapping("/products")
+ public ResponseEntity> getProducts(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "READ_PRODUCTS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_PRODUCTS permission required"));
+ }
+
+ List products = productRepository.findAll();
+ List> result = products.stream().map(p -> {
+ Map map = new HashMap<>();
+ map.put("id", p.getId());
+ map.put("productId", p.getProductId());
+ map.put("name", p.getName());
+ map.put("category", p.getCategory());
+ map.put("price", p.getPrice()); // List price is fine for catalog, not a financial report
+ map.put("stock", p.getStock());
+ map.put("active", p.isActive());
+ map.put("isDraft", p.isDraft());
+ return map;
+ }).collect(Collectors.toList());
+
+ return ResponseEntity.ok(result);
+ }
+
+ @GetMapping("/wallet")
+ public ResponseEntity> getWallet(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @RequestParam(required = false) String mobileNumber,
+ @RequestParam(required = false) Long userId) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "READ_WALLETS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
+ }
+
+ // ── Check if checking balance for another user by mobileNumber or userId ──
+ if (mobileNumber != null || userId != null) {
+ Optional targetUserOpt = Optional.empty();
+ if (userId != null) {
+ targetUserOpt = userRepository.findById(userId);
+ } else {
+ targetUserOpt = userRepository.findByMobileNumber(mobileNumber);
+ }
+
+ if (targetUserOpt.isPresent()) {
+ User user = targetUserOpt.get();
+ return ResponseEntity.ok(Map.of(
+ "userMobile", user.getMobileNumber(),
+ "userName", user.getName() != null ? user.getName() : "Customer",
+ "ritzTokenBalance", user.getRitzTokenBalance(),
+ "currency", "Ritz Token",
+ "queryScope", "SPECIFIC_USER"
+ ));
+ } else {
+ return ResponseEntity.status(404).body(Map.of("error", "Target user not found"));
+ }
+ }
+
+ if ("CUSTOMER".equals(key.getUserType())) {
+ // Customer key: return their specific wallet balance
+ Optional userOpt = userRepository.findById(key.getUserId());
+ if (userOpt.isPresent()) {
+ User user = userOpt.get();
+ return ResponseEntity.ok(Map.of(
+ "userMobile", user.getMobileNumber(),
+ "userName", user.getName() != null ? user.getName() : "Customer",
+ "ritzTokenBalance", user.getRitzTokenBalance(),
+ "currency", "Ritz Token",
+ "queryScope", "OWNER"
+ ));
+ }
+ return ResponseEntity.status(404).body(Map.of("error", "Owner user not found"));
+ } else {
+ // System key: return overall token circulation stats, but NO individual user accounts or detailed ledgers
+ List allUsers = userRepository.findAll();
+ BigDecimal totalCirculation = allUsers.stream()
+ .map(User::getRitzTokenBalance)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ return ResponseEntity.ok(Map.of(
+ "scope", "SYSTEM_CIRCULATION",
+ "activeWalletsCount", allUsers.size(),
+ "totalCirculationBalance", totalCirculation,
+ "currency", "Ritz Token",
+ "queryScope", "SYSTEM"
+ ));
+ }
+ }
+
+ // ── 5. Get Orders (requires READ_ORDERS) ────────────────────────────────
+ @GetMapping("/orders")
+ public ResponseEntity> getOrders(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "READ_ORDERS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_ORDERS permission required"));
+ }
+
+ List orders;
+ if ("CUSTOMER".equals(key.getUserType())) {
+ orders = orderRepository.findByUserIdOrderByCreatedAtDesc(key.getUserId());
+ } else {
+ orders = orderRepository.findAll();
+ }
+
+ // Map to DTO. Financial details (totalAmount, paymentMethod, item prices) are only exposed for staff/SYSTEM keys.
+ List> result = orders.stream().map(o -> {
+ Map map = new HashMap<>();
+ map.put("id", o.getId());
+ map.put("orderNumber", o.getOrderNumber());
+ map.put("displayOrderId", o.getDisplayOrderId());
+ map.put("status", o.getStatus());
+ map.put("createdAt", o.getCreatedAt());
+ map.put("orderType", o.getOrderType());
+
+ if ("SYSTEM".equalsIgnoreCase(key.getUserType())) {
+ map.put("totalAmount", o.getTotalAmount());
+ map.put("paymentMethod", o.getPaymentMethod());
+ }
+
+ List> itemsList = o.getItems().stream().map(item -> {
+ Map itemMap = new HashMap<>();
+ itemMap.put("productName", item.getProductName());
+ itemMap.put("quantity", item.getQuantity());
+ itemMap.put("stallName", item.getStallName());
+ if ("SYSTEM".equalsIgnoreCase(key.getUserType())) {
+ itemMap.put("price", item.getPrice());
+ }
+ return itemMap;
+ }).collect(Collectors.toList());
+
+ map.put("items", itemsList);
+ return map;
+ }).collect(Collectors.toList());
+
+ return ResponseEntity.ok(result);
+ }
+
+ // ── 5a. Get Wallet Transactions (requires READ_WALLETS & SYSTEM key) ─────
+ @GetMapping("/wallet/transactions")
+ public ResponseEntity> getWalletTransactions(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: Only staff keys can view general transaction records"));
+ }
+ if (!checkPermission(key, "READ_WALLETS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
+ }
+ return ResponseEntity.ok(tokenService.getAllTransactions());
+ }
+
+ // ── 5b. Get Wallet Circulation Stats (requires READ_WALLETS & SYSTEM key) ──
+ @GetMapping("/wallet/stats")
+ public ResponseEntity> getWalletStats(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: Only staff keys can view wallet statistics"));
+ }
+ if (!checkPermission(key, "READ_WALLETS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
+ }
+ return ResponseEntity.ok(tokenService.getGlobalStats());
+ }
+
+ // ── 6. Create Stall (requires WRITE_STALLS) ───────────────────────────
+ @PostMapping("/stalls")
+ public ResponseEntity> createStall(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @RequestBody Stall stall) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_STALLS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
+ }
+ Stall saved = stallRepository.save(stall);
+ return ResponseEntity.status(201).body(saved);
+ }
+
+ // ── 7. Update Stall (requires WRITE_STALLS) ───────────────────────────
+ @PutMapping("/stalls/{id}")
+ @Transactional
+ public ResponseEntity> updateStall(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @PathVariable Long id,
+ @RequestBody Stall updatedStall) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_STALLS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
+ }
+ return stallRepository.findById(id).map(stall -> {
+ stall.setName(updatedStall.getName());
+ stall.setDescription(updatedStall.getDescription());
+ stall.setActive(updatedStall.isActive());
+ stall.setTemporarilyClosed(updatedStall.isTemporarilyClosed());
+ stall.setSessionOptional(updatedStall.isSessionOptional());
+ return ResponseEntity.ok(stallRepository.save(stall));
+ }).orElse(ResponseEntity.notFound().build());
+ }
+
+ // ── 8. Delete Stall (requires WRITE_STALLS) ───────────────────────────
+ @DeleteMapping("/stalls/{id}")
+ public ResponseEntity> deleteStall(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @PathVariable Long id) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_STALLS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
+ }
+ return stallRepository.findById(id).map(stall -> {
+ stallRepository.delete(stall);
+ return ResponseEntity.ok(Map.of("success", true, "message", "Stall deleted successfully"));
+ }).orElse(ResponseEntity.notFound().build());
+ }
+
+ // ── 9. Create Product (requires WRITE_PRODUCTS) ───────────────────────
+ @PostMapping("/products")
+ @Transactional
+ public ResponseEntity> createProduct(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @RequestBody Product product) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_PRODUCTS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
+ }
+ Product saved = productRepository.save(product);
+ if (saved.isDraft()) {
+ notificationService.createNotification(
+ "Draft Product Created via API",
+ "A product draft '" + saved.getName() + "' was created via developer API key.",
+ "PRODUCT",
+ "/inventory/products"
+ );
+ }
+ return ResponseEntity.status(201).body(saved);
+ }
+
+ // ── 10. Update Product (requires WRITE_PRODUCTS) ───────────────────────
+ @PutMapping("/products/{id}")
+ @Transactional
+ public ResponseEntity> updateProduct(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @PathVariable Long id,
+ @RequestBody Product details) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_PRODUCTS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
+ }
+ return productRepository.findById(id).map(p -> {
+ p.setProductId(details.getProductId());
+ p.setName(details.getName());
+ p.setCategory(details.getCategory());
+ p.setDescription(details.getDescription());
+ p.setPrice(details.getPrice());
+ p.setStock(details.getStock());
+ p.setActive(details.isActive());
+ p.setVeg(details.isVeg());
+
+ Product updated = productRepository.save(p);
+ stockUpdateController.broadcastStockUpdate(updated.getId(), updated.getStock());
+ return ResponseEntity.ok(updated);
+ }).orElse(ResponseEntity.notFound().build());
+ }
+
+ // ── 11. Delete Product (requires WRITE_PRODUCTS) ──────────────────────
+ @DeleteMapping("/products/{id}")
+ public ResponseEntity> deleteProduct(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @PathVariable Long id) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_PRODUCTS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
+ }
+ return productRepository.findById(id).map(p -> {
+ productRepository.delete(p);
+ return ResponseEntity.ok(Map.of("success", true, "message", "Product deleted successfully"));
+ }).orElse(ResponseEntity.notFound().build());
+ }
+
+ // ── 12. Update Order Status (requires WRITE_ORDERS) ───────────────────
+ @PatchMapping("/orders/{id}/status")
+ @Transactional
+ public ResponseEntity> updateOrderStatus(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @PathVariable Long id,
+ @RequestBody Map body) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_ORDERS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_ORDERS permission required"));
+ }
+ String newStatus = body.get("status");
+ if (newStatus == null || newStatus.isEmpty()) {
+ return ResponseEntity.badRequest().body(Map.of("error", "Status field is required"));
+ }
+ return orderRepository.findById(id).map(order -> {
+ String oldStatus = order.getStatus();
+ String nextStatus = newStatus.toUpperCase();
+ if ("CANCELLED".equals(nextStatus) && !"CANCELLED".equals(oldStatus)) {
+ if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
+ tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
+ order.getTotalAmount(), "Status changed to CANCELLED via API");
+ }
+ }
+ order.setStatus(nextStatus);
+ orderRepository.save(order);
+ return ResponseEntity.ok(Map.of("success", true, "message", "Order status updated to " + nextStatus));
+ }).orElse(ResponseEntity.notFound().build());
+ }
+
+ // ── 13. Wallet Topup (requires WRITE_WALLETS) ──────────────────────────
+ @PostMapping("/wallet/topup")
+ @Transactional
+ public ResponseEntity> walletTopup(
+ @RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
+ @RequestBody Map body) {
+ DeveloperApiKey key = authenticate(headerKey);
+ if (key == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
+ }
+ if (!checkPermission(key, "WRITE_WALLETS")) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_WALLETS permission required"));
+ }
+
+ try {
+ Long targetUserId = null;
+ if (body.containsKey("userId") && body.get("userId") != null) {
+ targetUserId = Long.valueOf(body.get("userId").toString());
+ } else if (body.containsKey("mobileNumber") && body.get("mobileNumber") != null) {
+ String mobile = body.get("mobileNumber").toString();
+ User targetUser = userRepository.findByMobileNumber(mobile).orElse(null);
+ if (targetUser != null) {
+ targetUserId = targetUser.getId();
+ } else {
+ return ResponseEntity.status(404).body(Map.of("error", "User with mobile number " + mobile + " not found"));
+ }
+ } else {
+ return ResponseEntity.badRequest().body(Map.of("error", "userId or mobileNumber is required"));
+ }
+
+ BigDecimal amount = new BigDecimal(body.get("amount").toString());
+ if (amount.compareTo(new BigDecimal("50")) < 0) {
+ return ResponseEntity.badRequest().body(Map.of("error", "Minimum top up amount is 50 tokens"));
+ }
+ if (amount.compareTo(new BigDecimal("5000")) > 0) {
+ return ResponseEntity.badRequest().body(Map.of("error", "Single transaction limit exceeded (Max: 5000 tokens)"));
+ }
+
+ String ref = body.getOrDefault("referenceId", "API-TOPUP-" + System.currentTimeMillis()).toString();
+ User updatedUser = tokenService.topUp(targetUserId, amount, ref);
+ return ResponseEntity.ok(Map.of(
+ "success", true,
+ "newBalance", updatedUser.getRitzTokenBalance(),
+ "message", "Successfully added " + amount + " Ritz Tokens via API Key"
+ ));
+ } catch (Exception e) {
+ return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
+ }
+ }
+}
diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiKeyController.java b/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiKeyController.java
new file mode 100644
index 00000000..706b4909
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/controller/DeveloperApiKeyController.java
@@ -0,0 +1,120 @@
+package com.rit.canteen.sales.controller;
+
+import com.rit.canteen.sales.model.DeveloperApiKey;
+import com.rit.canteen.sales.service.DeveloperApiKeyService;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/developer-keys")
+public class DeveloperApiKeyController {
+
+ @Autowired
+ private DeveloperApiKeyService keyService;
+
+ @GetMapping
+ public ResponseEntity> listKeys() {
+ UserContext context = getUserContext();
+ if (context == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
+ }
+ List keys = keyService.getKeysForUser(context.userId, context.userType);
+ return ResponseEntity.ok(keys);
+ }
+
+ @PostMapping
+ public ResponseEntity> createKey(@RequestBody Map body) {
+ UserContext context = getUserContext();
+ if (context == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
+ }
+ String name = body.containsKey("name") && body.get("name") != null
+ ? body.get("name").toString()
+ : "My API Key";
+
+ boolean writeAccess = false;
+ String permissions = "";
+
+ if ("SYSTEM".equalsIgnoreCase(context.userType)) {
+ Object writeVal = body.get("writeAccess");
+ if (writeVal instanceof Boolean) {
+ writeAccess = (Boolean) writeVal;
+ } else if (writeVal != null) {
+ writeAccess = Boolean.parseBoolean(writeVal.toString());
+ }
+ permissions = body.containsKey("permissions") && body.get("permissions") != null
+ ? body.get("permissions").toString()
+ : "";
+ } else {
+ permissions = "READ_PRODUCTS,READ_STALLS,READ_ORDERS,READ_WALLETS";
+ }
+
+ try {
+ DeveloperApiKey newKey = keyService.createKey(context.userId, context.userType, context.identifier, name, writeAccess, permissions);
+ return ResponseEntity.status(201).body(newKey);
+ } catch (IllegalStateException e) {
+ return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
+ }
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity> deleteKey(@PathVariable Long id) {
+ UserContext context = getUserContext();
+ if (context == null) {
+ return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
+ }
+ try {
+ keyService.deleteKey(id, context.userId, context.userType);
+ return ResponseEntity.ok(Map.of("success", true, "message", "API Key revoked successfully"));
+ } catch (SecurityException e) {
+ return ResponseEntity.status(403).body(Map.of("error", e.getMessage()));
+ }
+ }
+
+ // ── Helper UserContext parser ──────────────────────────────────────────
+
+ private UserContext getUserContext() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null || !auth.isAuthenticated()) return null;
+
+ if (auth.getDetails() instanceof Claims claims) {
+ Long userId;
+ Object uid = claims.get("userId");
+ if (uid instanceof Integer) {
+ userId = ((Integer) uid).longValue();
+ } else if (uid instanceof Long) {
+ userId = (Long) uid;
+ } else if (uid != null) {
+ userId = Long.valueOf(uid.toString());
+ } else {
+ return null;
+ }
+
+ String type = (String) claims.get("type");
+ String userType = "customer".equals(type) ? "CUSTOMER" : "SYSTEM";
+ String identifier = claims.getSubject();
+
+ return new UserContext(userId, userType, identifier);
+ }
+ return null;
+ }
+
+ private static class UserContext {
+ final Long userId;
+ final String userType;
+ final String identifier;
+
+ UserContext(Long userId, String userType, String identifier) {
+ this.userId = userId;
+ this.userType = userType;
+ this.identifier = identifier;
+ }
+ }
+}
diff --git a/backend/src/main/java/com/rit/canteen/sales/model/DeveloperApiKey.java b/backend/src/main/java/com/rit/canteen/sales/model/DeveloperApiKey.java
new file mode 100644
index 00000000..4ed932ef
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/model/DeveloperApiKey.java
@@ -0,0 +1,137 @@
+package com.rit.canteen.sales.model;
+
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "developer_api_keys")
+public class DeveloperApiKey {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String apiKey;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private Long userId;
+
+ @Column(nullable = false)
+ private String userType; // "SYSTEM" or "CUSTOMER"
+
+ @Column(nullable = false)
+ private String ownerIdentifier; // Email or Mobile Number
+
+ @Column(nullable = false)
+ private boolean active = true;
+
+ @Column(nullable = false, columnDefinition = "boolean default false")
+ private boolean writeAccess = false;
+
+ @Column(nullable = true, length = 1000)
+ private String permissions; // comma-separated scopes (e.g. READ_PRODUCTS,WRITE_PRODUCTS)
+
+ @Column(nullable = false)
+ private LocalDateTime createdAt;
+
+ private LocalDateTime lastUsedAt;
+
+ public DeveloperApiKey() {}
+
+ @PrePersist
+ protected void onCreate() {
+ createdAt = LocalDateTime.now();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getUserType() {
+ return userType;
+ }
+
+ public void setUserType(String userType) {
+ this.userType = userType;
+ }
+
+ public String getOwnerIdentifier() {
+ return ownerIdentifier;
+ }
+
+ public void setOwnerIdentifier(String ownerIdentifier) {
+ this.ownerIdentifier = ownerIdentifier;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getLastUsedAt() {
+ return lastUsedAt;
+ }
+
+ public void setLastUsedAt(LocalDateTime lastUsedAt) {
+ this.lastUsedAt = lastUsedAt;
+ }
+
+ public boolean isWriteAccess() {
+ return writeAccess;
+ }
+
+ public void setWriteAccess(boolean writeAccess) {
+ this.writeAccess = writeAccess;
+ }
+
+ public String getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(String permissions) {
+ this.permissions = permissions;
+ }
+}
diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/DeveloperApiKeyRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/DeveloperApiKeyRepository.java
new file mode 100644
index 00000000..3f9ba815
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/repository/DeveloperApiKeyRepository.java
@@ -0,0 +1,12 @@
+package com.rit.canteen.sales.repository;
+
+import com.rit.canteen.sales.model.DeveloperApiKey;
+import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.List;
+import java.util.Optional;
+
+public interface DeveloperApiKeyRepository extends JpaRepository {
+ Optional findByApiKey(String apiKey);
+ List findByUserIdAndUserType(Long userId, String userType);
+ long countByUserIdAndUserType(Long userId, String userType);
+}
diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DeveloperApiKeyService.java b/backend/src/main/java/com/rit/canteen/sales/service/DeveloperApiKeyService.java
new file mode 100644
index 00000000..276c279f
--- /dev/null
+++ b/backend/src/main/java/com/rit/canteen/sales/service/DeveloperApiKeyService.java
@@ -0,0 +1,74 @@
+package com.rit.canteen.sales.service;
+
+import com.rit.canteen.sales.model.DeveloperApiKey;
+import com.rit.canteen.sales.repository.DeveloperApiKeyRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+public class DeveloperApiKeyService {
+
+ @Autowired
+ private DeveloperApiKeyRepository repository;
+
+ @Transactional(readOnly = true)
+ public List getKeysForUser(Long userId, String userType) {
+ return repository.findByUserIdAndUserType(userId, userType);
+ }
+
+ @Transactional
+ public DeveloperApiKey createKey(Long userId, String userType, String ownerIdentifier, String name, boolean writeAccess, String permissions) {
+ // Enforce the 3 key limit only for regular CUSTOMER users. SYSTEM users (admin/managers) get unlimited keys.
+ if (!"SYSTEM".equalsIgnoreCase(userType)) {
+ long count = repository.countByUserIdAndUserType(userId, userType);
+ if (count >= 3) {
+ throw new IllegalStateException("Maximum limit of 3 API keys reached");
+ }
+ }
+
+ DeveloperApiKey key = new DeveloperApiKey();
+ key.setUserId(userId);
+ key.setUserType(userType);
+ key.setOwnerIdentifier(ownerIdentifier);
+ key.setName(name);
+ key.setWriteAccess(writeAccess);
+ key.setPermissions(permissions);
+
+ // Generate a secure API key
+ String prefix = writeAccess ? "DEV-W-" : "DEV-";
+ String rawKey = prefix + UUID.randomUUID().toString().replace("-", "").toUpperCase();
+ key.setApiKey(rawKey);
+
+ return repository.save(key);
+ }
+
+ @Transactional
+ public void deleteKey(Long keyId, Long userId, String userType) {
+ Optional keyOpt = repository.findById(keyId);
+ if (keyOpt.isPresent()) {
+ DeveloperApiKey key = keyOpt.get();
+ if (key.getUserId().equals(userId) && key.getUserType().equals(userType)) {
+ repository.delete(key);
+ } else {
+ throw new SecurityException("Unauthorized to delete this API key");
+ }
+ }
+ }
+
+ @Transactional
+ public Optional validateAndUseKey(String apiKey) {
+ Optional keyOpt = repository.findByApiKey(apiKey);
+ if (keyOpt.isPresent() && keyOpt.get().isActive()) {
+ DeveloperApiKey key = keyOpt.get();
+ key.setLastUsedAt(LocalDateTime.now());
+ return Optional.of(repository.save(key));
+ }
+ return Optional.empty();
+ }
+}