diff --git a/.gitignore b/.gitignore index 98d39036..caeb3fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,8 @@ group-ironmen-master/*.* group-ironmen-tracker-master/*.* tasks-tracker-plugin-master/*.* os-league-tools-master/build.tar.gz + +node_modules/ +.vscode/ +.gitea/ +.claude/ diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 00000000..d9adf309 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,45 @@ +module.exports = { + apps: [ + { + name: 'api-prod', + cwd: './server', + script: 'npx', + args: 'tsx src/index.ts', + env: { + NODE_ENV: 'production', + PORT: 3002, + DATABASE_URL: 'file:./data.db', + SESSION_SECRET: 'K7xP2mN9qR4sT6vW8yB1cD3fG5hJ0kL2nO4pQ6rS8tU0wX2z', + BACKEND_SECRET: 'A1bC3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC9dE1f', + CORS_ORIGINS: 'https://leagues.tools,https://www.leagues.tools,http://localhost:3002', + FRONTEND_BUILD_PATH: '../os-league-tools-master/build', + }, + }, + { + name: 'api-dev', + cwd: './server', + script: 'npx', + args: 'tsx watch src/index.ts', + env: { + NODE_ENV: 'development', + PORT: 3003, + DATABASE_URL: 'file:./data-dev.db', + SESSION_SECRET: 'K7xP2mN9qR4sT6vW8yB1cD3fG5hJ0kL2nO4pQ6rS8tU0wX2z', + BACKEND_SECRET: 'A1bC3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC9dE1f', + CORS_ORIGINS: 'http://localhost:3000,http://localhost:3001,https://dev.leagues.tools', + FRONTEND_BUILD_PATH: '../os-league-tools-master/build', + }, + }, + { + name: 'frontend-dev', + cwd: './os-league-tools-master', + script: 'npm', + args: 'run dev', + env: { + BROWSER: 'none', + PORT: 3000, + WDS_SOCKET_PORT: 443, // Use nginx's HTTPS port for WebSocket + }, + }, + ], +}; diff --git a/os-league-tools-master/.env.development b/os-league-tools-master/.env.development new file mode 100644 index 00000000..04bd9b69 --- /dev/null +++ b/os-league-tools-master/.env.development @@ -0,0 +1,7 @@ +# Development environment variables + +# API server - use same domain, nginx proxies /api to backend +REACT_APP_RELDO_URL= + +# Google Analytics (optional - leave empty for dev) +REACT_APP_GA_MID= diff --git a/os-league-tools-master/.env.example b/os-league-tools-master/.env.example new file mode 100644 index 00000000..b2868a62 --- /dev/null +++ b/os-league-tools-master/.env.example @@ -0,0 +1,5 @@ +# API server URL +REACT_APP_RELDO_URL=http://localhost:3001 + +# Google Analytics tracking ID (optional) +REACT_APP_GA_MID= diff --git a/os-league-tools-master/.env.production b/os-league-tools-master/.env.production new file mode 100644 index 00000000..c6b7809d --- /dev/null +++ b/os-league-tools-master/.env.production @@ -0,0 +1,7 @@ +# Production environment variables + +# Production API server (same origin - served by Node.js) +REACT_APP_RELDO_URL= + +# Google Analytics +REACT_APP_GA_MID= diff --git a/os-league-tools-master/leagues-tools-frontend-dev.service b/os-league-tools-master/leagues-tools-frontend-dev.service new file mode 100644 index 00000000..c7153463 --- /dev/null +++ b/os-league-tools-master/leagues-tools-frontend-dev.service @@ -0,0 +1,19 @@ +[Unit] +Description=Leagues Tools Frontend Dev Server (Hot Reload) +After=network.target leagues-tools-dev.service +Wants=leagues-tools-dev.service + +[Service] +Type=simple +User=sonder +WorkingDirectory=/home/sonder/leagues-tools-dev/os-league-tools-master +ExecStart=/usr/bin/npm run dev +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=leagues-tools-frontend-dev +Environment=NODE_ENV=development + +[Install] +WantedBy=multi-user.target diff --git a/os-league-tools-master/package-lock.json b/os-league-tools-master/package-lock.json index 53759971..e3252593 100644 --- a/os-league-tools-master/package-lock.json +++ b/os-league-tools-master/package-lock.json @@ -96,6 +96,7 @@ }, "devDependencies": { "gh-pages": "^6.2.0", + "http-proxy-middleware": "^3.0.5", "husky": "^7.0.4", "jest": "^27.4.7", "lint-staged": "^12.1.5", @@ -10254,26 +10255,21 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/https-proxy-agent": { @@ -10858,6 +10854,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-port-reachable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", @@ -19831,6 +19837,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", diff --git a/os-league-tools-master/package.json b/os-league-tools-master/package.json index 6d2a1fe2..d9763aff 100644 --- a/os-league-tools-master/package.json +++ b/os-league-tools-master/package.json @@ -108,6 +108,7 @@ }, "devDependencies": { "gh-pages": "^6.2.0", + "http-proxy-middleware": "^3.0.5", "husky": "^7.0.4", "jest": "^27.4.7", "lint-staged": "^12.1.5", diff --git a/os-league-tools-master/src/App.js b/os-league-tools-master/src/App.js index 59c9b9f9..39c7f08c 100644 --- a/os-league-tools-master/src/App.js +++ b/os-league-tools-master/src/App.js @@ -19,6 +19,7 @@ import Faq from './pages/Faq'; import ViewCharacter from './pages/ViewCharacter'; import Groups from './pages/Groups'; import Planner from './pages/Planner'; +import Admin from './pages/Admin'; import { submitRenderError } from './client/feedback-client'; import { ErrorPage } from './components/common/util/ErrorBoundary'; @@ -45,6 +46,7 @@ history.listen(() => { const isDevEnvironment = () => window.location.hostname.startsWith('dev.') || window.location.hostname === 'localhost' || + window.location.port === '3000' || window.location.port === '3001'; export default function App() { @@ -111,6 +113,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/os-league-tools-master/src/client/admin-client.js b/os-league-tools-master/src/client/admin-client.js new file mode 100644 index 00000000..fc3cc9fb --- /dev/null +++ b/os-league-tools-master/src/client/admin-client.js @@ -0,0 +1,95 @@ +// Use relative URLs when REACT_APP_RELDO_URL is not set (same-origin via nginx) +const BASE_URL = process.env.REACT_APP_RELDO_URL || ''; +const DEFAULT_HEADERS = { + 'Content-type': 'application/json', +}; + +function handleResponse(response) { + if (!response.ok) { + return response.json().then(data => ({ + success: false, + error: data.message || data.error || 'An error occurred', + })); + } + return response.json().then(data => ({ + success: true, + value: data, + })); +} + +function handleError(error) { + console.warn(error); + return { success: false, error: error.message || 'Network error' }; +} + +export function getAdminStats() { + return fetch(`${BASE_URL}/api/admin/stats`, { + method: 'GET', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +export function getUsers() { + return fetch(`${BASE_URL}/api/admin/users`, { + method: 'GET', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +export function getUser(id) { + return fetch(`${BASE_URL}/api/admin/users/${id}`, { + method: 'GET', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +export function updateUser(id, data) { + return fetch(`${BASE_URL}/api/admin/users/${id}`, { + method: 'PATCH', + headers: DEFAULT_HEADERS, + credentials: 'include', + body: JSON.stringify(data), + }) + .then(handleResponse) + .catch(handleError); +} + +export function resetUserPassword(id, password) { + return fetch(`${BASE_URL}/api/admin/users/${id}/password`, { + method: 'PATCH', + headers: DEFAULT_HEADERS, + credentials: 'include', + body: JSON.stringify({ password }), + }) + .then(handleResponse) + .catch(handleError); +} + +export function deleteUser(id) { + return fetch(`${BASE_URL}/api/admin/users/${id}`, { + method: 'DELETE', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +export function invalidateUserSessions(id) { + return fetch(`${BASE_URL}/api/admin/users/${id}/sessions`, { + method: 'DELETE', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} diff --git a/os-league-tools-master/src/client/auth-client.js b/os-league-tools-master/src/client/auth-client.js index 659dcb7d..b92f8367 100644 --- a/os-league-tools-master/src/client/auth-client.js +++ b/os-league-tools-master/src/client/auth-client.js @@ -1,4 +1,5 @@ -const BASE_URL = process.env.REACT_APP_RELDO_URL || 'http://localhost:8080'; +// Use relative URLs when REACT_APP_RELDO_URL is not set (same-origin via nginx) +const BASE_URL = process.env.REACT_APP_RELDO_URL || ''; const DEFAULT_HEADERS = { 'Content-type': 'application/json', }; diff --git a/os-league-tools-master/src/client/character-client.js b/os-league-tools-master/src/client/character-client.js new file mode 100644 index 00000000..f0a03ad3 --- /dev/null +++ b/os-league-tools-master/src/client/character-client.js @@ -0,0 +1,98 @@ +// Use relative URLs when REACT_APP_RELDO_URL is not set (same-origin via nginx) +const BASE_URL = process.env.REACT_APP_RELDO_URL || ''; +const DEFAULT_HEADERS = { + 'Content-type': 'application/json', +}; + +function handleResponse(response) { + if (!response.ok) { + return response.json().then(data => ({ + success: false, + error: data.message || data.error || 'An error occurred', + })); + } + return response.json().then(data => ({ + success: true, + value: data, + })); +} + +function handleError(error) { + console.warn(error); + return { success: false, error: error.message || 'Network error' }; +} + +/** + * Get all characters for the authenticated user + */ +export function getCharacters() { + return fetch(`${BASE_URL}/api/characters`, { + method: 'GET', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +/** + * Create a new character + * @param {string} rsn - RuneScape Name + * @param {boolean} setActive - Whether to set this character as active + */ +export function createCharacter(rsn, setActive = false) { + return fetch(`${BASE_URL}/api/characters`, { + method: 'POST', + headers: DEFAULT_HEADERS, + credentials: 'include', + body: JSON.stringify({ rsn, setActive }), + }) + .then(handleResponse) + .catch(handleError); +} + +/** + * Update a character (rename, set active, or sync data) + * @param {number} id - Character ID + * @param {object} updates - Fields to update (rsn, isActive, tasksData, unlocksData, notesData) + */ +export function updateCharacter(id, updates) { + return fetch(`${BASE_URL}/api/characters/${id}`, { + method: 'PATCH', + headers: DEFAULT_HEADERS, + credentials: 'include', + body: JSON.stringify(updates), + }) + .then(handleResponse) + .catch(handleError); +} + +/** + * Delete a character + * @param {number} id - Character ID + */ +export function deleteCharacter(id) { + return fetch(`${BASE_URL}/api/characters/${id}`, { + method: 'DELETE', + headers: DEFAULT_HEADERS, + credentials: 'include', + }) + .then(handleResponse) + .catch(handleError); +} + +/** + * Bulk sync characters (used on login to merge local data with server) + * @param {Array} characters - Array of character objects or RSN strings + * @param {number} activeIndex - Index of the active character + */ +export function syncCharacters(characters, activeIndex) { + return fetch(`${BASE_URL}/api/characters/sync`, { + method: 'POST', + headers: DEFAULT_HEADERS, + credentials: 'include', + body: JSON.stringify({ characters, activeIndex }), + }) + .then(handleResponse) + .catch(handleError); +} diff --git a/os-league-tools-master/src/client/hiscores-client.js b/os-league-tools-master/src/client/hiscores-client.js index 9116fe54..93c56236 100644 --- a/os-league-tools-master/src/client/hiscores-client.js +++ b/os-league-tools-master/src/client/hiscores-client.js @@ -1,4 +1,5 @@ -const BASE_URL = process.env.REACT_APP_RELDO_URL || 'http://localhost:8080'; +// Use relative URLs when REACT_APP_RELDO_URL is not set (same-origin via nginx) +const BASE_URL = process.env.REACT_APP_RELDO_URL || ''; export default async function getHiscores(rsn, handleResultCallback) { if (!rsn) { diff --git a/os-league-tools-master/src/components/PageWrapper.js b/os-league-tools-master/src/components/PageWrapper.js index 5e6d6788..77a9861d 100644 --- a/os-league-tools-master/src/components/PageWrapper.js +++ b/os-league-tools-master/src/components/PageWrapper.js @@ -20,11 +20,11 @@ export default function PageWrapper({ children }) { const [manageDataModalType, setManageDataModalType] = useQueryString('open'); const navItems = [ - new NavItem('Stats', 'primary', 0, 0).withRouterLink('/stats').withIconFont('query_stats'), - new NavItem('Trackers', 'primary', 0, 1).withRouterLink('/tracker').withIconFont('checklist_rtl'), - new NavItem('Calculators', 'primary', 0, 2).withRouterLink('/calculators').withIconFont('calculate'), - new NavItem('Groups', 'primary', 0, 3).withRouterLink('/groups').withIconFont('groups'), - new NavItem('Planner', 'primary', 0, 4).withRouterLink('/planner').withIconFont('event_note'), + new NavItem('WIP-Stats', 'primary', 0, 0).withRouterLink('/stats').withIconFont('query_stats'), + new NavItem('WIP-Trackers', 'primary', 0, 1).withRouterLink('/tracker').withIconFont('checklist_rtl'), + new NavItem('WIP-Calculators', 'primary', 0, 2).withRouterLink('/calculators').withIconFont('calculate'), + new NavItem('WIP-Groups', 'primary', 0, 3).withRouterLink('/groups').withIconFont('groups'), + new NavItem('WIP-Planner', 'primary', 0, 4).withRouterLink('/planner').withIconFont('event_note'), new NavItem('Settings', 'overflow', 3, 1).withRouterLink('/settings').withIconFont('settings'), new NavItem('WIP-FAQ', 'overflow', 3, 2).withRouterLink('/faq').withIconFont('help_outline'), new NavItem('WIP-About', 'overflow', 3, 3).withRouterLink('/about').withIconFont('info'), diff --git a/os-league-tools-master/src/components/nav/AuthButton.js b/os-league-tools-master/src/components/nav/AuthButton.js index 34759737..ce533a30 100644 --- a/os-league-tools-master/src/components/nav/AuthButton.js +++ b/os-league-tools-master/src/components/nav/AuthButton.js @@ -1,4 +1,5 @@ import React, { useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; import Spinner from '../common/Spinner'; import Dropdown from '../common/Dropdown'; import useAccount from '../../hooks/useAccount'; @@ -13,6 +14,7 @@ function NavBarItem() { isAuthenticating, username, userEmail, + isAdmin, openAuthModal, logout, authModalOpen, @@ -70,6 +72,13 @@ function NavBarItem() { {userEmail &&

{userEmail}

} + {isAdmin && ( + setExpanded(false)}> + + Admin + + + )} {userEmail} )} + {isAdmin && ( + { + setExpanded(false); + if (onNavigate) { + onNavigate(); + } + }} + > + admin_panel_settings + Admin + + )} + + + ) : ( + <> + + + + + + )} + + {showPasswordReset && ( +
+ setNewPassword(e.target.value)} + placeholder='New password' + className='input-primary text-sm flex-1' + /> + +
+ )} + {message && ( +
+ {message.text} +
+ )} + + + ); +} + +export default function Admin() { + const { isLoggedIn, isAuthenticating, isAdmin } = useAccount(); + const [stats, setStats] = useState(null); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + const [statsResult, usersResult] = await Promise.all([getAdminStats(), getUsers()]); + + if (statsResult.success) { + setStats(statsResult.value); + } else { + setError(statsResult.error); + } + + if (usersResult.success) { + setUsers(usersResult.value.users); + } else { + setError(usersResult.error); + } + + setLoading(false); + }; + + useEffect(() => { + if (isAdmin) { + fetchData(); + } + }, [isAdmin]); + + // Redirect non-admins + if (!isAuthenticating && (!isLoggedIn || !isAdmin)) { + return ; + } + + return ( + +
+

+ admin_panel_settings + Admin Dashboard +

+ + {loading && ( +
+ sync +

Loading...

+
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!loading && stats && ( + <> + {/* Stats Cards */} +
+ + + + +
+ + {/* Users Table */} +
+
+

+ people + Users ({users.length}) +

+
+
+ + + + + + + + + + + + {users.map(user => ( + + ))} + +
+ User + + Role + + Sessions + + Created + + Actions +
+
+
+ + )} +
+
+ ); +} diff --git a/os-league-tools-master/src/setupProxy.js b/os-league-tools-master/src/setupProxy.js new file mode 100644 index 00000000..ad94f0cf --- /dev/null +++ b/os-league-tools-master/src/setupProxy.js @@ -0,0 +1,15 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function(app) { + // Proxy API requests to the Node.js backend + app.use( + '/api', + createProxyMiddleware({ + target: process.env.REACT_APP_RELDO_URL || 'http://localhost:3003', + changeOrigin: true, + secure: false, + // Pass cookies for session auth + cookieDomainRewrite: 'localhost', + }) + ); +}; diff --git a/os-league-tools-master/src/store/updateWithUserDataStorage.js b/os-league-tools-master/src/store/updateWithUserDataStorage.js index 91891175..06975fab 100644 --- a/os-league-tools-master/src/store/updateWithUserDataStorage.js +++ b/os-league-tools-master/src/store/updateWithUserDataStorage.js @@ -1,25 +1,49 @@ -/* eslint-disable no-unused-vars */ +import { LOCALSTORAGE_KEYS } from '../client/localstorage-client'; +import { updateCharacter } from '../client/character-client'; + +// Debounce server sync to avoid too many requests +let syncTimeout = null; +const SYNC_DEBOUNCE_MS = 2000; + +// eslint-disable-next-line no-unused-vars export default function updateWithUserDataStorage(wrappedDispatchFn, wrappedFnProps, localstorageKey, stateKey) { return async (dispatch, getState) => { await dispatch(wrappedDispatchFn(wrappedFnProps)); - // TODO re-enable user login - // const dataKey = (() => { - // switch (localstorageKey) { - // case LOCALSTORAGE_KEYS.UNLOCKS: - // case LOCALSTORAGE_KEYS.TASKS: { - // const state = getState(); - // return `${localstorageKey}_${state.character.characters[state.character.activeCharacter] ?? 'DEFAULT'}`; - // } - // default: { - // return localstorageKey; - // } - // } - // })(); + const state = getState(); + const { isLoggedIn } = state.account.accountCache; - // const { isLoggedIn, userEmail, accessToken } = getState().account.accountCache; - // if (isLoggedIn && !wrappedFnProps.skipDbUpdate) { - // putUserData(userEmail, dataKey, getState()[stateKey], accessToken); - // } + // Sync to server if logged in and not skipping DB update + if (isLoggedIn && !wrappedFnProps?.skipDbUpdate) { + // Only sync tasks and unlocks data + if (localstorageKey === LOCALSTORAGE_KEYS.TASKS || localstorageKey === LOCALSTORAGE_KEYS.UNLOCKS) { + const activeRsn = state.character.characters[state.character.activeCharacter]; + const characterId = state.character.characterIds[activeRsn]; + + if (characterId) { + // Debounce the sync to avoid too many requests + if (syncTimeout) { + clearTimeout(syncTimeout); + } + + syncTimeout = setTimeout(async () => { + const currentState = getState(); + const updateData = {}; + + if (localstorageKey === LOCALSTORAGE_KEYS.TASKS) { + updateData.tasksData = currentState.tasks; + } else if (localstorageKey === LOCALSTORAGE_KEYS.UNLOCKS) { + updateData.unlocksData = currentState.unlocks; + } + + try { + await updateCharacter(characterId, updateData); + } catch (error) { + console.warn('Failed to sync data to server:', error); + } + }, SYNC_DEBOUNCE_MS); + } + } + } }; } diff --git a/os-league-tools-master/src/store/user/account.js b/os-league-tools-master/src/store/user/account.js index 2bd67726..a2ab47f1 100644 --- a/os-league-tools-master/src/store/user/account.js +++ b/os-league-tools-master/src/store/user/account.js @@ -2,7 +2,7 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; -export const CURRENT_VERSION = 4; +export const CURRENT_VERSION = 5; const INITIAL_STATE = { version: CURRENT_VERSION, @@ -11,6 +11,7 @@ const INITIAL_STATE = { isChecking: true, username: undefined, userEmail: undefined, + userRole: undefined, }, }; @@ -26,12 +27,14 @@ export const accountSlice = createSlice({ state.accountCache.isChecking = false; state.accountCache.username = action.payload.username; state.accountCache.userEmail = action.payload.email; + state.accountCache.userRole = action.payload.role; }, setLoggedOut: state => { state.accountCache.isLoggedIn = false; state.accountCache.isChecking = false; state.accountCache.username = undefined; state.accountCache.userEmail = undefined; + state.accountCache.userRole = undefined; }, reset: () => INITIAL_STATE, }, diff --git a/os-league-tools-master/src/store/user/character.js b/os-league-tools-master/src/store/user/character.js index a33e13a5..e106d7bc 100644 --- a/os-league-tools-master/src/store/user/character.js +++ b/os-league-tools-master/src/store/user/character.js @@ -16,11 +16,18 @@ export const characterSlice = createSlice({ }, addCharacter: (state, action) => { state.characters.push(action.payload.rsn); + if (action.payload.id) { + state.characterIds[action.payload.rsn] = action.payload.id; + } if (action.payload.setActive) { state.activeCharacter = state.characters.length - 1; } }, deleteCharacter: (state, action) => { + const rsn = state.characters[action.payload]; + if (rsn && state.characterIds[rsn]) { + delete state.characterIds[rsn]; + } state.characters.splice(action.payload, 1); if (state.activeCharacter === action.payload) { state.activeCharacter = 0; @@ -29,8 +36,28 @@ export const characterSlice = createSlice({ } }, renameCharacter: (state, action) => { + const oldRsn = state.characters[action.payload.index]; + const serverId = state.characterIds[oldRsn]; + if (serverId) { + delete state.characterIds[oldRsn]; + state.characterIds[action.payload.rsn] = serverId; + } state.characters[action.payload.index] = action.payload.rsn; }, + // Sync characters from server (replaces local state) + syncFromServer: (state, action) => { + const { characters, activeIndex } = action.payload; + state.characters = characters.map(c => c.rsn); + state.characterIds = {}; + characters.forEach(c => { + state.characterIds[c.rsn] = c.id; + }); + state.activeCharacter = activeIndex ?? 0; + }, + // Set server ID for a character + setCharacterId: (state, action) => { + state.characterIds[action.payload.rsn] = action.payload.id; + }, updateHiscores: (state, action) => { switch (action.payload.type) { case 'LOADING': @@ -128,6 +155,6 @@ export function reset(props) { return updateWithUserDataStorage(innerReset, props, LOCALSTORAGE_KEYS.CHARACTER, 'character'); } -export const { updateHiscores } = characterSlice.actions; +export const { updateHiscores, syncFromServer, setCharacterId } = characterSlice.actions; export default characterSlice.reducer; diff --git a/os-league-tools-master/src/store/user/constants.js b/os-league-tools-master/src/store/user/constants.js index 5422c4b6..623a4d6c 100644 --- a/os-league-tools-master/src/store/user/constants.js +++ b/os-league-tools-master/src/store/user/constants.js @@ -1,4 +1,4 @@ -export const CURRENT_VERSION = 2; +export const CURRENT_VERSION = 3; export const HISCORES_TTL = 1800000; // 30 min in ms @@ -13,5 +13,7 @@ export const INITIAL_STATE = { version: CURRENT_VERSION, activeCharacter: 0, characters: [], + // Map of RSN -> server character ID (for sync) + characterIds: {}, hiscoresCache: INITIAL_HISCORES_STATE, }; diff --git a/os-league-tools-master/src/store/user/updateCharacterVersion.js b/os-league-tools-master/src/store/user/updateCharacterVersion.js index 3949637b..328d77db 100644 --- a/os-league-tools-master/src/store/user/updateCharacterVersion.js +++ b/os-league-tools-master/src/store/user/updateCharacterVersion.js @@ -2,6 +2,7 @@ import { CURRENT_VERSION } from './constants'; const versionUpdaters = { 2: updateToV2, + 3: updateToV3, }; export default function updateCharacterVersion(state) { @@ -27,3 +28,12 @@ function updateToV2(prevState) { characters: [prevState.username], }; } + +function updateToV3(prevState) { + // V3 adds characterIds map for server sync + return { + ...prevState, + version: 3, + characterIds: {}, + }; +} diff --git a/os-league-tools-master/src/styles/compiled.css b/os-league-tools-master/src/styles/compiled.css index 3ae5724a..af17e628 100644 --- a/os-league-tools-master/src/styles/compiled.css +++ b/os-league-tools-master/src/styles/compiled.css @@ -947,6 +947,16 @@ body:is(.dark *) { border-color: rgb(113 113 122 / var(--tw-border-opacity, 1)); } +.border-error { + --tw-border-opacity: 1; + border-color: rgb(220 38 38 / var(--tw-border-opacity, 1)); +} + +.border-error:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + .shadow-primary { --tw-shadow-color: rgb(0 0 0 / 0.1); --tw-shadow: var(--tw-shadow-colored); @@ -2742,6 +2752,9 @@ select[multiple]:focus option:checked { .mb-6 { margin-bottom: 1.5rem; } +.mb-8 { + margin-bottom: 2rem; +} .mb-auto { margin-bottom: auto; } @@ -2976,6 +2989,9 @@ select[multiple]:focus option:checked { .max-w-5xl { max-width: 64rem; } +.max-w-6xl { + max-width: 72rem; +} .max-w-\[3\.5rem\] { max-width: 3.5rem; } @@ -3185,6 +3201,9 @@ select[multiple]:focus option:checked { .overflow-hidden { overflow: hidden; } +.overflow-x-auto { + overflow-x: auto; +} .overflow-y-auto { overflow-y: auto; } @@ -3561,6 +3580,10 @@ select[multiple]:focus option:checked { .tracking-widest { letter-spacing: 0.1em; } +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} .text-blue-400 { --tw-text-opacity: 1; color: rgb(96 165 250 / var(--tw-text-opacity, 1)); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..afaa12d9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1672 @@ +{ + "name": "leagues-tools-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "pm2": "^6.0.14" + } + }, + "node_modules/@pm2/agent": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz", + "integrity": "sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==", + "dev": true, + "license": "AGPL-3.0", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.1.0", + "fclone": "~1.0.11", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.4.0", + "semver": "~7.5.0", + "ws": "~7.5.10" + } + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@pm2/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==", + "dev": true, + "license": "MIT", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@pm2/io": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.1.0.tgz", + "integrity": "sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==", + "dev": true, + "license": "Apache-2", + "dependencies": { + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/js-api": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz", + "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==", + "dev": true, + "license": "Apache-2", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.0.0-node10", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz", + "integrity": "sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types/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" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bodec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", + "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dev": true, + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/culvert": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", + "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz", + "integrity": "sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==", + "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/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.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/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/extrareqp2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", + "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/git-node-fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", + "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/git-sha1": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz", + "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "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/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-git": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", + "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.14.tgz", + "integrity": "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==", + "dev": true, + "license": "AGPL-3.0", + "dependencies": { + "@pm2/agent": "~2.1.1", + "@pm2/blessed": "0.1.81", + "@pm2/io": "~6.1.0", + "@pm2/js-api": "~0.8.0", + "@pm2/pm2-version-check": "^1.0.4", + "ansis": "4.0.0-node10", + "async": "3.2.6", + "chokidar": "3.6.0", + "cli-tableau": "2.0.1", + "commander": "2.15.1", + "croner": "4.1.97", + "dayjs": "1.11.15", + "debug": "4.4.3", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "js-yaml": "4.1.1", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "3.0.2", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "2.2.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2-axon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz", + "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz", + "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2-sysmonit": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz", + "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==", + "dev": true, + "license": "Apache", + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/systeminformation": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.6.tgz", + "integrity": "sha512-LEIyK1aEv5P3BhAPW3swdlIyCihxwEq/Gki+kcONieU4PIeRCSLDuGkk0Va/56PSBgjVgEksOM88dmY6YqOyfQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "dev": true, + "license": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/vizion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", + "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..50e53c26 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "leagues-tools-dev", + "private": true, + "scripts": { + "start": "npx pm2 start ecosystem.config.js", + "stop": "npx pm2 stop all", + "restart": "npx pm2 restart all", + "status": "npx pm2 status", + "logs": "npx pm2 logs", + "logs:prod": "npx pm2 logs api-prod", + "logs:dev": "npx pm2 logs api-dev", + "logs:frontend": "npx pm2 logs frontend-dev", + "kill": "npx pm2 kill" + }, + "devDependencies": { + "pm2": "^6.0.14" + } +} diff --git a/server/.env b/server/.env new file mode 100644 index 00000000..d46f20b9 --- /dev/null +++ b/server/.env @@ -0,0 +1,23 @@ +# Development environment +NODE_ENV=development + +# Database (separate dev database) +DATABASE_URL="file:./data-dev.db" + +# Server +PORT=3003 + +# Security +SESSION_SECRET="K7xP2mN9qR4sT6vW8yB1cD3fG5hJ0kL2nO4pQ6rS8tU0wX2z" +BACKEND_SECRET="A1bC3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC9dE1f" + +# CORS - allow React dev server +CORS_ORIGINS="http://localhost:3000,http://localhost:3001,https://dev.leagues.tools" + +# Captcha (disabled for dev) +CAPTCHA_ENABLED=false +CAPTCHA_SITEKEY="" +CAPTCHA_SECRET="" + +# Frontend build path (not used in dev - React dev server handles it) +FRONTEND_BUILD_PATH="../os-league-tools-master/build" diff --git a/server/.env.development b/server/.env.development new file mode 100644 index 00000000..d46f20b9 --- /dev/null +++ b/server/.env.development @@ -0,0 +1,23 @@ +# Development environment +NODE_ENV=development + +# Database (separate dev database) +DATABASE_URL="file:./data-dev.db" + +# Server +PORT=3003 + +# Security +SESSION_SECRET="K7xP2mN9qR4sT6vW8yB1cD3fG5hJ0kL2nO4pQ6rS8tU0wX2z" +BACKEND_SECRET="A1bC3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC9dE1f" + +# CORS - allow React dev server +CORS_ORIGINS="http://localhost:3000,http://localhost:3001,https://dev.leagues.tools" + +# Captcha (disabled for dev) +CAPTCHA_ENABLED=false +CAPTCHA_SITEKEY="" +CAPTCHA_SECRET="" + +# Frontend build path (not used in dev - React dev server handles it) +FRONTEND_BUILD_PATH="../os-league-tools-master/build" diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..108d59f4 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Database +*.db +*.db-journal + +# Environment +.env.local +.env.production + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Prisma +prisma/migrations/ diff --git a/server/leagues-tools-dev.service b/server/leagues-tools-dev.service new file mode 100644 index 00000000..a6e9938f --- /dev/null +++ b/server/leagues-tools-dev.service @@ -0,0 +1,18 @@ +[Unit] +Description=Leagues Tools Development Server +After=network.target + +[Service] +Type=simple +User=sonder +WorkingDirectory=/home/sonder/leagues-tools-dev/server +ExecStart=/usr/bin/npm run dev +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=leagues-tools-dev +Environment=NODE_ENV=development + +[Install] +WantedBy=multi-user.target diff --git a/server/leagues-tools-prod.service b/server/leagues-tools-prod.service new file mode 100644 index 00000000..6bfa32b7 --- /dev/null +++ b/server/leagues-tools-prod.service @@ -0,0 +1,18 @@ +[Unit] +Description=Leagues Tools Production Server +After=network.target + +[Service] +Type=simple +User=sonder +WorkingDirectory=/home/sonder/leagues-tools-dev/server +ExecStart=/usr/bin/npm run prod +Restart=on-failure +RestartSec=10 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=leagues-tools-prod +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..ef661db9 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1708 @@ +{ + "name": "leagues-tools-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "leagues-tools-server", + "version": "1.0.0", + "dependencies": { + "@hono/node-server": "^1.13.7", + "@prisma/client": "^6.2.1", + "bcrypt": "^5.1.1", + "blakejs": "^1.2.1", + "hono": "^4.6.16", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/node": "^22.10.5", + "@types/uuid": "^10.0.0", + "prisma": "^6.2.1", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": 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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "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/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", + "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", + "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..dcdefa5f --- /dev/null +++ b/server/package.json @@ -0,0 +1,35 @@ +{ + "name": "leagues-tools-server", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "cp .env.development .env && tsx watch src/index.ts", + "prod": "cp .env.production .env && tsx src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "start:dev": "cp .env.development .env && node dist/index.js", + "start:prod": "cp .env.production .env && node dist/index.js", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:push:dev": "cp .env.development .env && prisma db push", + "db:push:prod": "cp .env.production .env && prisma db push", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio" + }, + "dependencies": { + "@hono/node-server": "^1.13.7", + "@prisma/client": "^6.2.1", + "blakejs": "^1.2.1", + "bcrypt": "^5.1.1", + "hono": "^4.6.16", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/node": "^22.10.5", + "@types/uuid": "^10.0.0", + "prisma": "^6.2.1", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 00000000..5443b44a --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,215 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +// User roles +enum Role { + USER + ADMIN +} + +// User authentication +model User { + id Int @id @default(autoincrement()) + username String @unique + email String @unique + passwordHash String + role Role @default(USER) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sessions Session[] + characters Character[] + ownedGroups Group[] @relation("GroupOwner") +} + +// User's OSRS characters (for task/unlock tracking) +model Character { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + rsn String // RuneScape Name + isActive Boolean @default(false) + + // Synced data (stored as JSON) + tasksData String? // JSON - task completion status + unlocksData String? // JSON - unlock status + notesData String? // JSON - user notes/planner data + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, rsn]) + @@index([userId]) +} + +model Session { + id String @id @default(uuid()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + expiresAt DateTime + createdAt DateTime @default(now()) + + @@index([userId]) +} + +// Group tracking (for RuneLite plugin) +model Group { + id Int @id @default(autoincrement()) + name String + tokenHash String // Blake2b-256 hash of token + version Int @default(1) + ownerId Int? // Optional - user who owns/manages this group + owner User? @relation("GroupOwner", fields: [ownerId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + members Member[] + + @@unique([name, tokenHash]) + @@index([tokenHash]) + @@index([ownerId]) +} + +model Member { + id Int @id @default(autoincrement()) + groupId Int + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + name String + + // Stats (HP, Prayer, Energy, World, etc.) - JSON array of 7 integers + stats String? // JSON + statsLastUpdate DateTime? + + // Coordinates (x, y, plane) - JSON array of 3 integers + coordinates String? // JSON + coordinatesLastUpdate DateTime? + + // Skills (24 skills) - JSON array of 24 integers + skills String? // JSON + skillsLastUpdate DateTime? + + // Quests - binary blob + quests Bytes? + questsLastUpdate DateTime? + + // Inventory (56 items) - JSON array of 56 integers + inventory String? // JSON + inventoryLastUpdate DateTime? + + // Equipment (28 slots) - JSON array of 28 integers + equipment String? // JSON + equipmentLastUpdate DateTime? + + // Rune pouch (8 runes) - JSON array of 8 integers + runePouch String? // JSON + runePouchLastUpdate DateTime? + + // Bank - JSON array (variable length) + bank String? // JSON + bankLastUpdate DateTime? + + // Seed vault - JSON array (variable length) + seedVault String? // JSON + seedVaultLastUpdate DateTime? + + // Interacting NPC + interacting String? + interactingLastUpdate DateTime? + + // Diary vars (62 integers) - JSON array + diaryVars String? // JSON + diaryVarsLastUpdate DateTime? + + // Overall last update + lastUpdated DateTime? + + // Skills aggregation + skillsDay SkillsDay[] + skillsMonth SkillsMonth[] + skillsYear SkillsYear[] + + // Collection log + collectionLogs CollectionLog[] + collectionLogsNew CollectionLogNew[] + + @@unique([groupId, name]) + @@index([groupId]) +} + +// Skills aggregation tables +model SkillsDay { + memberId Int + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + time DateTime + skills String // JSON array of 24 integers + + @@id([memberId, time]) +} + +model SkillsMonth { + memberId Int + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + time DateTime + skills String // JSON array of 24 integers + + @@id([memberId, time]) +} + +model SkillsYear { + memberId Int + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + time DateTime + skills String // JSON array of 24 integers + + @@id([memberId, time]) +} + +// Aggregation tracking +model AggregationInfo { + type String @id + lastAggregation DateTime @default(dbgenerated("'2000-01-01 00:00:00'")) +} + +// Collection log tables +model CollectionTab { + id Int @id @default(autoincrement()) + name String + pages CollectionPage[] +} + +model CollectionPage { + id Int @id @default(autoincrement()) + tabId Int + tab CollectionTab @relation(fields: [tabId], references: [id], onDelete: Cascade) + pageName String + collectionLogs CollectionLog[] + collectionLogsNew CollectionLogNew[] + + @@unique([tabId, pageName]) +} + +model CollectionLog { + memberId Int + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + pageId Int + page CollectionPage @relation(fields: [pageId], references: [id], onDelete: Cascade) + items String? // JSON array of item IDs + counts String? // JSON array of completion counts + lastUpdated DateTime? + + @@id([memberId, pageId]) +} + +model CollectionLogNew { + memberId Int + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + pageId Int + page CollectionPage @relation(fields: [pageId], references: [id], onDelete: Cascade) + newItems String? // JSON array of new item IDs + lastUpdated DateTime? + + @@id([memberId, pageId]) +} diff --git a/server/src/app.ts b/server/src/app.ts new file mode 100644 index 00000000..160d3c84 --- /dev/null +++ b/server/src/app.ts @@ -0,0 +1,58 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { serveStatic } from '@hono/node-server/serve-static'; +import { sessionMiddleware } from './middleware/session'; +import authRoutes from './routes/auth'; +import publicRoutes from './routes/public'; +import groupRoutes from './routes/groups'; +import memberRoutes from './routes/members'; +import adminRoutes from './routes/admin'; +import characterRoutes from './routes/characters'; + +export function createApp() { + const app = new Hono(); + + // Middleware + app.use('*', logger()); + + // CORS - configured via CORS_ORIGINS env var + const corsOrigins = process.env.CORS_ORIGINS + ? process.env.CORS_ORIGINS.split(',') + : ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:4000']; + + app.use( + '*', + cors({ + origin: corsOrigins, + credentials: true, + allowHeaders: ['Content-Type', 'Authorization'], + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], + maxAge: 3600, + }) + ); + + // Session middleware for all routes + app.use('*', sessionMiddleware); + + // API Routes + app.route('/api', authRoutes); + app.route('/api', publicRoutes); + app.route('/api/group', groupRoutes); + app.route('/api/group', memberRoutes); + app.route('/api/admin', adminRoutes); + app.route('/api/characters', characterRoutes); + + // Health check + app.get('/api/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() })); + + // Serve static files from React build + const frontendPath = process.env.FRONTEND_BUILD_PATH || '../os-league-tools-master/build'; + + app.use('/*', serveStatic({ root: frontendPath })); + + // SPA fallback - serve index.html for all non-API routes + app.get('*', serveStatic({ path: `${frontendPath}/index.html` })); + + return app; +} diff --git a/server/src/db.ts b/server/src/db.ts new file mode 100644 index 00000000..348534c4 --- /dev/null +++ b/server/src/db.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); + +export async function connectDatabase() { + try { + await prisma.$connect(); + console.log('Database connected'); + } catch (error) { + console.error('Database connection failed:', error); + process.exit(1); + } +} + +export async function disconnectDatabase() { + await prisma.$disconnect(); +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 00000000..e9328f19 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,39 @@ +import { serve } from '@hono/node-server'; +import { createApp } from './app'; +import { connectDatabase, disconnectDatabase } from './db'; + +const PORT = parseInt(process.env.PORT || '3001', 10); + +async function main() { + // Connect to database + await connectDatabase(); + + // Create app + const app = createApp(); + + // Start server + console.log(`Server starting on port ${PORT}...`); + + serve({ + fetch: app.fetch, + port: PORT, + }); + + console.log(`Server running at http://localhost:${PORT}`); + console.log(`API available at http://localhost:${PORT}/api`); + + // Graceful shutdown + const shutdown = async () => { + console.log('\nShutting down...'); + await disconnectDatabase(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +main().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/server/src/middleware/groupAuth.ts b/server/src/middleware/groupAuth.ts new file mode 100644 index 00000000..a6455698 --- /dev/null +++ b/server/src/middleware/groupAuth.ts @@ -0,0 +1,52 @@ +import { Context, Next } from 'hono'; +import { prisma } from '../db'; +import { hashToken } from '../utils/blake2'; + +// Extend Hono context with group data +declare module 'hono' { + interface ContextVariableMap { + groupId: number | null; + groupName: string | null; + } +} + +/** + * Group token authentication middleware. + * Validates the Authorization header token against the group. + * + * Expected header format: Authorization: {token} + * Group name is extracted from the URL path parameter. + */ +export async function groupAuthMiddleware(c: Context, next: Next) { + const groupName = c.req.param('group_name'); + const token = c.req.header('Authorization'); + + if (!groupName) { + return c.json({ error: 'Group name required' }, 400); + } + + if (!token) { + return c.json({ error: 'Authorization token required' }, 401); + } + + // Hash the token with the group name as salt + const tokenHash = hashToken(token, groupName); + + // Find the group with matching name and token hash + const group = await prisma.group.findFirst({ + where: { + name: groupName, + tokenHash: tokenHash, + }, + }); + + if (!group) { + return c.json({ error: 'Invalid token or group not found' }, 401); + } + + // Set group info in context + c.set('groupId', group.id); + c.set('groupName', group.name); + + await next(); +} diff --git a/server/src/middleware/session.ts b/server/src/middleware/session.ts new file mode 100644 index 00000000..205c746d --- /dev/null +++ b/server/src/middleware/session.ts @@ -0,0 +1,129 @@ +import { Context, Next } from 'hono'; +import { getCookie, setCookie, deleteCookie } from 'hono/cookie'; +import { Role } from '@prisma/client'; +import { prisma } from '../db'; + +const SESSION_COOKIE_NAME = 'session_id'; +const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days + +export interface SessionUser { + id: number; + username: string; + email: string; + role: Role; +} + +// Extend Hono context with session data +declare module 'hono' { + interface ContextVariableMap { + user: SessionUser | null; + sessionId: string | null; + } +} + +/** + * Session middleware - validates session cookie and sets user in context + */ +export async function sessionMiddleware(c: Context, next: Next) { + const sessionId = getCookie(c, SESSION_COOKIE_NAME); + + if (sessionId) { + const session = await prisma.session.findUnique({ + where: { id: sessionId }, + include: { user: true }, + }); + + if (session && session.expiresAt > new Date()) { + c.set('user', { + id: session.user.id, + username: session.user.username, + email: session.user.email, + role: session.user.role, + }); + c.set('sessionId', sessionId); + } else if (session) { + // Session expired, clean it up + await prisma.session.delete({ where: { id: sessionId } }); + deleteCookie(c, SESSION_COOKIE_NAME); + c.set('user', null); + c.set('sessionId', null); + } else { + c.set('user', null); + c.set('sessionId', null); + } + } else { + c.set('user', null); + c.set('sessionId', null); + } + + await next(); +} + +/** + * Create a new session for a user + */ +export async function createSession(c: Context, userId: number): Promise { + const expiresAt = new Date(Date.now() + SESSION_MAX_AGE); + + const session = await prisma.session.create({ + data: { + userId, + expiresAt, + }, + }); + + setCookie(c, SESSION_COOKIE_NAME, session.id, { + path: '/', + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'Lax', + maxAge: SESSION_MAX_AGE / 1000, + }); + + return session.id; +} + +/** + * Destroy the current session + */ +export async function destroySession(c: Context): Promise { + const sessionId = c.get('sessionId'); + + if (sessionId) { + await prisma.session.delete({ where: { id: sessionId } }).catch(() => {}); + } + + deleteCookie(c, SESSION_COOKIE_NAME); + c.set('user', null); + c.set('sessionId', null); +} + +/** + * Middleware to require authentication + */ +export async function requireAuth(c: Context, next: Next) { + const user = c.get('user'); + + if (!user) { + return c.json({ error: 'Unauthorized' }, 401); + } + + await next(); +} + +/** + * Middleware to require admin role + */ +export async function requireAdmin(c: Context, next: Next) { + const user = c.get('user'); + + if (!user) { + return c.json({ error: 'Unauthorized' }, 401); + } + + if (user.role !== 'ADMIN') { + return c.json({ error: 'Forbidden: Admin access required' }, 403); + } + + await next(); +} diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts new file mode 100644 index 00000000..4bd8e73a --- /dev/null +++ b/server/src/routes/admin.ts @@ -0,0 +1,221 @@ +import { Hono } from 'hono'; +import { prisma } from '../db'; +import { requireAdmin } from '../middleware/session'; +import { hashPassword } from '../utils/password'; + +const admin = new Hono(); + +// All admin routes require admin role +admin.use('/*', requireAdmin); + +/** + * GET /api/admin/users + * List all users + */ +admin.get('/users', async (c) => { + const users = await prisma.user.findMany({ + select: { + id: true, + username: true, + email: true, + role: true, + createdAt: true, + updatedAt: true, + _count: { + select: { sessions: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + return c.json({ + users: users.map((u) => ({ + ...u, + sessionCount: u._count.sessions, + _count: undefined, + })), + }); +}); + +/** + * GET /api/admin/users/:id + * Get single user details + */ +admin.get('/users/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10); + + const user = await prisma.user.findUnique({ + where: { id }, + select: { + id: true, + username: true, + email: true, + role: true, + createdAt: true, + updatedAt: true, + sessions: { + select: { + id: true, + createdAt: true, + expiresAt: true, + }, + }, + }, + }); + + if (!user) { + return c.json({ error: 'User not found' }, 404); + } + + return c.json({ user }); +}); + +/** + * PATCH /api/admin/users/:id + * Update user (role, email, etc.) + */ +admin.patch('/users/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10); + const body = await c.req.json(); + const { role, email, username } = body; + + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) { + return c.json({ error: 'User not found' }, 404); + } + + const updateData: Record = {}; + + if (role && ['USER', 'ADMIN'].includes(role)) { + updateData.role = role; + } + + if (email) { + // Check if email is already taken + const existing = await prisma.user.findFirst({ + where: { email, NOT: { id } }, + }); + if (existing) { + return c.json({ error: 'Email already in use' }, 400); + } + updateData.email = email; + } + + if (username) { + // Check if username is already taken + const existing = await prisma.user.findFirst({ + where: { username, NOT: { id } }, + }); + if (existing) { + return c.json({ error: 'Username already in use' }, 400); + } + updateData.username = username; + } + + const updated = await prisma.user.update({ + where: { id }, + data: updateData, + select: { + id: true, + username: true, + email: true, + role: true, + updatedAt: true, + }, + }); + + return c.json({ user: updated }); +}); + +/** + * PATCH /api/admin/users/:id/password + * Reset user password + */ +admin.patch('/users/:id/password', async (c) => { + const id = parseInt(c.req.param('id'), 10); + const body = await c.req.json(); + const { password } = body; + + if (!password || password.length < 8) { + return c.json({ error: 'Password must be at least 8 characters' }, 400); + } + + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) { + return c.json({ error: 'User not found' }, 404); + } + + const passwordHash = await hashPassword(password); + + await prisma.user.update({ + where: { id }, + data: { passwordHash }, + }); + + // Invalidate all sessions for this user + await prisma.session.deleteMany({ where: { userId: id } }); + + return c.json({ success: true, message: 'Password reset and sessions invalidated' }); +}); + +/** + * DELETE /api/admin/users/:id + * Delete user + */ +admin.delete('/users/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10); + const currentUser = c.get('user')!; + + // Prevent self-deletion + if (currentUser.id === id) { + return c.json({ error: 'Cannot delete your own account' }, 400); + } + + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) { + return c.json({ error: 'User not found' }, 404); + } + + await prisma.user.delete({ where: { id } }); + + return c.json({ success: true, message: 'User deleted' }); +}); + +/** + * DELETE /api/admin/users/:id/sessions + * Invalidate all sessions for a user + */ +admin.delete('/users/:id/sessions', async (c) => { + const id = parseInt(c.req.param('id'), 10); + + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) { + return c.json({ error: 'User not found' }, 404); + } + + const result = await prisma.session.deleteMany({ where: { userId: id } }); + + return c.json({ success: true, message: `${result.count} sessions invalidated` }); +}); + +/** + * GET /api/admin/stats + * Get admin dashboard stats + */ +admin.get('/stats', async (c) => { + const [userCount, sessionCount, groupCount, memberCount] = await Promise.all([ + prisma.user.count(), + prisma.session.count(), + prisma.group.count(), + prisma.member.count(), + ]); + + return c.json({ + users: userCount, + activeSessions: sessionCount, + groups: groupCount, + members: memberCount, + }); +}); + +export default admin; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts new file mode 100644 index 00000000..5f02f03e --- /dev/null +++ b/server/src/routes/auth.ts @@ -0,0 +1,135 @@ +import { Hono } from 'hono'; +import { prisma } from '../db'; +import { hashPassword, verifyPassword } from '../utils/password'; +import { createSession, destroySession, requireAuth } from '../middleware/session'; + +const auth = new Hono(); + +/** + * POST /api/register + * Create a new user account + */ +auth.post('/register', async (c) => { + const body = await c.req.json(); + const { username, email, password } = body; + + // Validation + if (!username || !email || !password) { + return c.json({ error: 'Username, email, and password are required' }, 400); + } + + if (username.length < 3 || username.length > 30) { + return c.json({ error: 'Username must be 3-30 characters' }, 400); + } + + if (password.length < 8) { + return c.json({ error: 'Password must be at least 8 characters' }, 400); + } + + // Check if username or email already exists + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ username }, { email }], + }, + }); + + if (existingUser) { + if (existingUser.username === username) { + return c.json({ error: 'Username already taken' }, 400); + } + return c.json({ error: 'Email already registered' }, 400); + } + + // Create user + const passwordHash = await hashPassword(password); + const user = await prisma.user.create({ + data: { + username, + email, + passwordHash, + }, + }); + + // Create session + await createSession(c, user.id); + + return c.json({ + username: user.username, + email: user.email, + role: user.role, + }); +}); + +/** + * POST /api/login + * Authenticate and create session + */ +auth.post('/login', async (c) => { + const body = await c.req.json(); + const { username, password } = body; + + if (!username || !password) { + return c.json({ error: 'Username and password are required' }, 400); + } + + // Find user by username or email + const user = await prisma.user.findFirst({ + where: { + OR: [{ username }, { email: username }], + }, + }); + + if (!user) { + return c.json({ error: 'Invalid username or password' }, 401); + } + + // Verify password + const isValid = await verifyPassword(password, user.passwordHash); + if (!isValid) { + return c.json({ error: 'Invalid username or password' }, 401); + } + + // Create session + await createSession(c, user.id); + + return c.json({ + username: user.username, + email: user.email, + role: user.role, + }); +}); + +/** + * POST /api/logout + * Destroy session + */ +auth.post('/logout', async (c) => { + await destroySession(c); + return c.json({ success: true }); +}); + +/** + * GET /api/auth/status + * Check if user is authenticated + */ +auth.get('/auth/status', (c) => { + const user = c.get('user'); + return c.json({ + authenticated: !!user, + }); +}); + +/** + * GET /api/me + * Get current user info + */ +auth.get('/me', requireAuth, (c) => { + const user = c.get('user'); + return c.json({ + username: user!.username, + email: user!.email, + role: user!.role, + }); +}); + +export default auth; diff --git a/server/src/routes/characters.ts b/server/src/routes/characters.ts new file mode 100644 index 00000000..e0466d29 --- /dev/null +++ b/server/src/routes/characters.ts @@ -0,0 +1,334 @@ +import { Hono } from 'hono'; +import { prisma } from '../db'; +import { requireAuth } from '../middleware/session'; + +const characters = new Hono(); + +// All character routes require authentication +characters.use('/*', requireAuth); + +/** + * GET /api/characters + * Get all characters for the authenticated user + */ +characters.get('/', async (c) => { + const user = c.get('user')!; + + const userCharacters = await prisma.character.findMany({ + where: { userId: user.id }, + orderBy: { createdAt: 'asc' }, + }); + + return c.json({ + characters: userCharacters.map((char) => ({ + id: char.id, + rsn: char.rsn, + isActive: char.isActive, + tasksData: char.tasksData ? JSON.parse(char.tasksData) : null, + unlocksData: char.unlocksData ? JSON.parse(char.unlocksData) : null, + notesData: char.notesData ? JSON.parse(char.notesData) : null, + createdAt: char.createdAt, + updatedAt: char.updatedAt, + })), + }); +}); + +/** + * POST /api/characters + * Create a new character + */ +characters.post('/', async (c) => { + const user = c.get('user')!; + const body = await c.req.json(); + const { rsn, setActive } = body; + + if (!rsn || typeof rsn !== 'string') { + return c.json({ error: 'RSN is required' }, 400); + } + + const trimmedRsn = rsn.trim(); + if (trimmedRsn.length < 1 || trimmedRsn.length > 12) { + return c.json({ error: 'RSN must be 1-12 characters' }, 400); + } + + // Check if character already exists for this user + const existing = await prisma.character.findUnique({ + where: { userId_rsn: { userId: user.id, rsn: trimmedRsn } }, + }); + + if (existing) { + return c.json({ error: 'Character already exists' }, 400); + } + + // If setActive, deactivate all other characters first + if (setActive) { + await prisma.character.updateMany({ + where: { userId: user.id }, + data: { isActive: false }, + }); + } + + const character = await prisma.character.create({ + data: { + userId: user.id, + rsn: trimmedRsn, + isActive: setActive || false, + }, + }); + + return c.json({ + character: { + id: character.id, + rsn: character.rsn, + isActive: character.isActive, + tasksData: null, + unlocksData: null, + notesData: null, + createdAt: character.createdAt, + updatedAt: character.updatedAt, + }, + }); +}); + +/** + * PATCH /api/characters/:id + * Update a character (rename, set active, sync data) + */ +characters.patch('/:id', async (c) => { + const user = c.get('user')!; + const id = parseInt(c.req.param('id'), 10); + const body = await c.req.json(); + + const character = await prisma.character.findFirst({ + where: { id, userId: user.id }, + }); + + if (!character) { + return c.json({ error: 'Character not found' }, 404); + } + + const updateData: Record = {}; + + // Rename + if (body.rsn !== undefined) { + const trimmedRsn = body.rsn.trim(); + if (trimmedRsn.length < 1 || trimmedRsn.length > 12) { + return c.json({ error: 'RSN must be 1-12 characters' }, 400); + } + // Check if new name already exists + const existing = await prisma.character.findFirst({ + where: { userId: user.id, rsn: trimmedRsn, NOT: { id } }, + }); + if (existing) { + return c.json({ error: 'Character with that name already exists' }, 400); + } + updateData.rsn = trimmedRsn; + } + + // Set active + if (body.isActive !== undefined) { + if (body.isActive) { + // Deactivate all other characters + await prisma.character.updateMany({ + where: { userId: user.id, NOT: { id } }, + data: { isActive: false }, + }); + } + updateData.isActive = body.isActive; + } + + // Sync tasks data + if (body.tasksData !== undefined) { + updateData.tasksData = body.tasksData ? JSON.stringify(body.tasksData) : null; + } + + // Sync unlocks data + if (body.unlocksData !== undefined) { + updateData.unlocksData = body.unlocksData ? JSON.stringify(body.unlocksData) : null; + } + + // Sync notes data + if (body.notesData !== undefined) { + updateData.notesData = body.notesData ? JSON.stringify(body.notesData) : null; + } + + const updated = await prisma.character.update({ + where: { id }, + data: updateData, + }); + + return c.json({ + character: { + id: updated.id, + rsn: updated.rsn, + isActive: updated.isActive, + tasksData: updated.tasksData ? JSON.parse(updated.tasksData) : null, + unlocksData: updated.unlocksData ? JSON.parse(updated.unlocksData) : null, + notesData: updated.notesData ? JSON.parse(updated.notesData) : null, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + }, + }); +}); + +/** + * DELETE /api/characters/:id + * Delete a character + */ +characters.delete('/:id', async (c) => { + const user = c.get('user')!; + const id = parseInt(c.req.param('id'), 10); + + const character = await prisma.character.findFirst({ + where: { id, userId: user.id }, + }); + + if (!character) { + return c.json({ error: 'Character not found' }, 404); + } + + await prisma.character.delete({ where: { id } }); + + // If deleted character was active, activate the first remaining character + if (character.isActive) { + const firstChar = await prisma.character.findFirst({ + where: { userId: user.id }, + orderBy: { createdAt: 'asc' }, + }); + if (firstChar) { + await prisma.character.update({ + where: { id: firstChar.id }, + data: { isActive: true }, + }); + } + } + + return c.json({ success: true }); +}); + +/** + * POST /api/characters/sync + * Bulk sync all characters (used on login to merge local data) + */ +characters.post('/sync', async (c) => { + const user = c.get('user')!; + const body = await c.req.json(); + const { characters: localCharacters, activeIndex } = body; + + if (!Array.isArray(localCharacters)) { + return c.json({ error: 'characters must be an array' }, 400); + } + + // Get existing server characters + const serverCharacters = await prisma.character.findMany({ + where: { userId: user.id }, + }); + + const serverRsnMap = new Map(serverCharacters.map((c) => [c.rsn.toLowerCase(), c])); + const result: Array<{ + id: number; + rsn: string; + isActive: boolean; + tasksData: unknown; + unlocksData: unknown; + notesData: unknown; + createdAt: Date; + updatedAt: Date; + }> = []; + + // Process each local character + for (let i = 0; i < localCharacters.length; i++) { + const local = localCharacters[i]; + const rsn = typeof local === 'string' ? local : local.rsn; + const tasksData = typeof local === 'object' ? local.tasksData : null; + const unlocksData = typeof local === 'object' ? local.unlocksData : null; + const notesData = typeof local === 'object' ? local.notesData : null; + const isActive = i === activeIndex; + + const existing = serverRsnMap.get(rsn.toLowerCase()); + + if (existing) { + // Update existing character if local data is newer/present + const updateData: Record = { isActive }; + + // Merge data - prefer local if it exists and server doesn't have it + if (tasksData && !existing.tasksData) { + updateData.tasksData = JSON.stringify(tasksData); + } + if (unlocksData && !existing.unlocksData) { + updateData.unlocksData = JSON.stringify(unlocksData); + } + if (notesData && !existing.notesData) { + updateData.notesData = JSON.stringify(notesData); + } + + const updated = await prisma.character.update({ + where: { id: existing.id }, + data: updateData, + }); + + result.push({ + id: updated.id, + rsn: updated.rsn, + isActive: updated.isActive, + tasksData: updated.tasksData ? JSON.parse(updated.tasksData) : null, + unlocksData: updated.unlocksData ? JSON.parse(updated.unlocksData) : null, + notesData: updated.notesData ? JSON.parse(updated.notesData) : null, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + }); + + serverRsnMap.delete(rsn.toLowerCase()); + } else { + // Create new character + const created = await prisma.character.create({ + data: { + userId: user.id, + rsn, + isActive, + tasksData: tasksData ? JSON.stringify(tasksData) : null, + unlocksData: unlocksData ? JSON.stringify(unlocksData) : null, + notesData: notesData ? JSON.stringify(notesData) : null, + }, + }); + + result.push({ + id: created.id, + rsn: created.rsn, + isActive: created.isActive, + tasksData: tasksData, + unlocksData: unlocksData, + notesData: notesData, + createdAt: created.createdAt, + updatedAt: created.updatedAt, + }); + } + } + + // Add remaining server characters that weren't in local + for (const serverChar of serverRsnMap.values()) { + // Deactivate if there was an active local character + if (serverChar.isActive && activeIndex !== undefined) { + await prisma.character.update({ + where: { id: serverChar.id }, + data: { isActive: false }, + }); + serverChar.isActive = false; + } + + result.push({ + id: serverChar.id, + rsn: serverChar.rsn, + isActive: serverChar.isActive, + tasksData: serverChar.tasksData ? JSON.parse(serverChar.tasksData) : null, + unlocksData: serverChar.unlocksData ? JSON.parse(serverChar.unlocksData) : null, + notesData: serverChar.notesData ? JSON.parse(serverChar.notesData) : null, + createdAt: serverChar.createdAt, + updatedAt: serverChar.updatedAt, + }); + } + + return c.json({ characters: result }); +}); + +export default characters; diff --git a/server/src/routes/groups.ts b/server/src/routes/groups.ts new file mode 100644 index 00000000..ac98006f --- /dev/null +++ b/server/src/routes/groups.ts @@ -0,0 +1,124 @@ +import { Hono } from 'hono'; +import { prisma } from '../db'; +import { groupAuthMiddleware } from '../middleware/groupAuth'; + +const groups = new Hono(); + +// Apply group token auth to all routes +groups.use('/*', groupAuthMiddleware); + +/** + * GET /api/group/:group_name/get-group-data + * Get all members with optional delta updates + */ +groups.get('/:group_name/get-group-data', async (c) => { + const groupId = c.get('groupId')!; + const fromTimeParam = c.req.query('from_time'); + + let fromTimestamp: Date | undefined; + if (fromTimeParam) { + // Try parsing as epoch milliseconds first, then ISO string + const epochMs = parseInt(fromTimeParam, 10); + if (!isNaN(epochMs)) { + fromTimestamp = new Date(epochMs); + } else { + fromTimestamp = new Date(fromTimeParam); + } + } + + const members = await prisma.member.findMany({ + where: { + groupId, + ...(fromTimestamp && { + lastUpdated: { gt: fromTimestamp }, + }), + }, + }); + + // Transform to API response format + const response = members.map((member) => ({ + name: member.name, + stats: member.stats ? JSON.parse(member.stats) : null, + statsLastUpdate: member.statsLastUpdate?.getTime() || null, + coordinates: member.coordinates ? JSON.parse(member.coordinates) : null, + coordinatesLastUpdate: member.coordinatesLastUpdate?.getTime() || null, + skills: member.skills ? JSON.parse(member.skills) : null, + skillsLastUpdate: member.skillsLastUpdate?.getTime() || null, + quests: member.quests ? Array.from(member.quests) : null, + questsLastUpdate: member.questsLastUpdate?.getTime() || null, + inventory: member.inventory ? JSON.parse(member.inventory) : null, + inventoryLastUpdate: member.inventoryLastUpdate?.getTime() || null, + equipment: member.equipment ? JSON.parse(member.equipment) : null, + equipmentLastUpdate: member.equipmentLastUpdate?.getTime() || null, + runePouch: member.runePouch ? JSON.parse(member.runePouch) : null, + runePouchLastUpdate: member.runePouchLastUpdate?.getTime() || null, + bank: member.bank ? JSON.parse(member.bank) : null, + bankLastUpdate: member.bankLastUpdate?.getTime() || null, + seedVault: member.seedVault ? JSON.parse(member.seedVault) : null, + seedVaultLastUpdate: member.seedVaultLastUpdate?.getTime() || null, + interacting: member.interacting, + interactingLastUpdate: member.interactingLastUpdate?.getTime() || null, + diaryVars: member.diaryVars ? JSON.parse(member.diaryVars) : null, + diaryVarsLastUpdate: member.diaryVarsLastUpdate?.getTime() || null, + lastUpdated: member.lastUpdated?.getTime() || null, + })); + + return c.json(response); +}); + +/** + * GET /api/group/:group_name/am-i-logged-in + * Check if authenticated (if this is reached, auth succeeded) + */ +groups.get('/:group_name/am-i-logged-in', (c) => { + return c.json({ authenticated: true }); +}); + +/** + * GET /api/group/:group_name/am-i-in-group + * Check if a member exists in the group + */ +groups.get('/:group_name/am-i-in-group', async (c) => { + const groupId = c.get('groupId')!; + const memberName = c.req.query('member_name'); + + if (!memberName) { + return c.json({ error: 'member_name is required' }, 400); + } + + const member = await prisma.member.findFirst({ + where: { + groupId, + name: memberName, + }, + }); + + return c.json({ in_group: !!member }); +}); + +/** + * GET /api/group/:group_name/get-skill-data + * Get skill aggregation data + * TODO: Implement skill aggregation + */ +groups.get('/:group_name/get-skill-data', async (c) => { + const groupId = c.get('groupId')!; + const period = c.req.query('period'); // day, month, year + + // TODO: Implement skill aggregation service + return c.json([]); +}); + +/** + * GET /api/group/:group_name/collection-log + * Get collection log data + * TODO: Implement collection log + */ +groups.get('/:group_name/collection-log', async (c) => { + const groupId = c.get('groupId')!; + + // TODO: Implement collection log service + return c.json([]); +}); + +export default groups; diff --git a/server/src/routes/members.ts b/server/src/routes/members.ts new file mode 100644 index 00000000..2b067aa1 --- /dev/null +++ b/server/src/routes/members.ts @@ -0,0 +1,211 @@ +import { Hono } from 'hono'; +import { prisma } from '../db'; +import { groupAuthMiddleware } from '../middleware/groupAuth'; + +const members = new Hono(); + +// Apply group token auth to all routes +members.use('/*', groupAuthMiddleware); + +/** + * POST /api/group/:group_name/update-group-member + * Update member data (main RuneLite plugin endpoint) + * Only non-null fields are updated + */ +members.post('/:group_name/update-group-member', async (c) => { + const groupId = c.get('groupId')!; + const body = await c.req.json(); + const { name, ...data } = body; + + if (!name) { + return c.json({ error: 'Member name is required' }, 400); + } + + // Find or create member + let member = await prisma.member.findFirst({ + where: { groupId, name }, + }); + + if (!member) { + // Auto-create member if doesn't exist + member = await prisma.member.create({ + data: { groupId, name }, + }); + } + + // Build update object with only provided fields + const now = new Date(); + const updateData: Record = { + lastUpdated: now, + }; + + if (data.stats !== undefined) { + updateData.stats = JSON.stringify(data.stats); + updateData.statsLastUpdate = now; + } + + if (data.coordinates !== undefined) { + updateData.coordinates = JSON.stringify(data.coordinates); + updateData.coordinatesLastUpdate = now; + } + + if (data.skills !== undefined) { + updateData.skills = JSON.stringify(data.skills); + updateData.skillsLastUpdate = now; + } + + if (data.quests !== undefined) { + updateData.quests = Buffer.from(data.quests); + updateData.questsLastUpdate = now; + } + + if (data.inventory !== undefined) { + updateData.inventory = JSON.stringify(data.inventory); + updateData.inventoryLastUpdate = now; + } + + if (data.equipment !== undefined) { + updateData.equipment = JSON.stringify(data.equipment); + updateData.equipmentLastUpdate = now; + } + + if (data.runePouch !== undefined || data.rune_pouch !== undefined) { + updateData.runePouch = JSON.stringify(data.runePouch || data.rune_pouch); + updateData.runePouchLastUpdate = now; + } + + if (data.bank !== undefined) { + updateData.bank = JSON.stringify(data.bank); + updateData.bankLastUpdate = now; + } + + if (data.seedVault !== undefined || data.seed_vault !== undefined) { + updateData.seedVault = JSON.stringify(data.seedVault || data.seed_vault); + updateData.seedVaultLastUpdate = now; + } + + if (data.interacting !== undefined) { + updateData.interacting = data.interacting; + updateData.interactingLastUpdate = now; + } + + if (data.diaryVars !== undefined || data.diary_vars !== undefined) { + updateData.diaryVars = JSON.stringify(data.diaryVars || data.diary_vars); + updateData.diaryVarsLastUpdate = now; + } + + // Update member + await prisma.member.update({ + where: { id: member.id }, + data: updateData, + }); + + return c.json({ status: 'success' }); +}); + +/** + * POST /api/group/:group_name/add-group-member + * Add a new member to the group + */ +members.post('/:group_name/add-group-member', async (c) => { + const groupId = c.get('groupId')!; + const body = await c.req.json(); + const { name } = body; + + if (!name) { + return c.json({ error: 'Member name is required' }, 400); + } + + // Check if member already exists + const existing = await prisma.member.findFirst({ + where: { groupId, name }, + }); + + if (existing) { + return c.json({ error: 'Member already exists in group' }, 400); + } + + // Create member + await prisma.member.create({ + data: { groupId, name }, + }); + + console.log(`Added member '${name}' to group_id: ${groupId}`); + + return c.json({ status: 'success' }); +}); + +/** + * DELETE /api/group/:group_name/delete-group-member + * Delete a member from the group + */ +members.delete('/:group_name/delete-group-member', async (c) => { + const groupId = c.get('groupId')!; + const body = await c.req.json(); + const { name } = body; + + if (!name) { + return c.json({ error: 'Member name is required' }, 400); + } + + const member = await prisma.member.findFirst({ + where: { groupId, name }, + }); + + if (!member) { + return c.json({ error: 'Member not found' }, 404); + } + + await prisma.member.delete({ + where: { id: member.id }, + }); + + console.log(`Deleted member '${name}' from group_id: ${groupId}`); + + return c.json({ status: 'success' }); +}); + +/** + * PUT /api/group/:group_name/rename-group-member + * Rename a group member + */ +members.put('/:group_name/rename-group-member', async (c) => { + const groupId = c.get('groupId')!; + const body = await c.req.json(); + const { originalName, newName, original_name, new_name } = body; + + const oldName = originalName || original_name; + const targetName = newName || new_name; + + if (!oldName || !targetName) { + return c.json({ error: 'originalName and newName are required' }, 400); + } + + const member = await prisma.member.findFirst({ + where: { groupId, name: oldName }, + }); + + if (!member) { + return c.json({ error: 'Member not found' }, 404); + } + + // Check if new name already exists + const existing = await prisma.member.findFirst({ + where: { groupId, name: targetName }, + }); + + if (existing) { + return c.json({ error: 'A member with that name already exists' }, 400); + } + + await prisma.member.update({ + where: { id: member.id }, + data: { name: targetName }, + }); + + console.log(`Renamed member '${oldName}' -> '${targetName}' in group_id: ${groupId}`); + + return c.json({ status: 'success' }); +}); + +export default members; diff --git a/server/src/routes/public.ts b/server/src/routes/public.ts new file mode 100644 index 00000000..d6c17842 --- /dev/null +++ b/server/src/routes/public.ts @@ -0,0 +1,121 @@ +import { Hono } from 'hono'; +import { v4 as uuidv4 } from 'uuid'; +import { prisma } from '../db'; +import { hashToken } from '../utils/blake2'; + +const publicRoutes = new Hono(); + +// In-memory cache for GE prices +let gePricesCache: Record = {}; +let gePricesCacheTime = 0; +const GE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes + +const CAPTCHA_ENABLED = process.env.CAPTCHA_ENABLED === 'true'; +const CAPTCHA_SITEKEY = process.env.CAPTCHA_SITEKEY || ''; + +/** + * POST /api/create-group + * Create a new group with a token + */ +publicRoutes.post('/create-group', async (c) => { + const body = await c.req.json(); + const { name, captchaResponse } = body; + + if (!name || typeof name !== 'string') { + return c.json({ error: 'Group name is required' }, 400); + } + + const trimmedName = name.trim(); + if (trimmedName.length < 1 || trimmedName.length > 100) { + return c.json({ error: 'Group name must be 1-100 characters' }, 400); + } + + // TODO: Implement captcha validation if enabled + // if (CAPTCHA_ENABLED && !verifyCaptcha(captchaResponse)) { + // return c.json({ error: 'Invalid captcha' }, 400); + // } + + // Generate token + const token = uuidv4(); + const tokenHash = hashToken(token, trimmedName); + + // Check if group name + token combination already exists + const existingGroup = await prisma.group.findFirst({ + where: { + name: trimmedName, + tokenHash: tokenHash, + }, + }); + + if (existingGroup) { + return c.json({ error: 'Group already exists' }, 400); + } + + // Create group + const group = await prisma.group.create({ + data: { + name: trimmedName, + tokenHash: tokenHash, + }, + }); + + console.log(`Group created: ${group.name} with token (first 8 chars): ${token.substring(0, 8)}...`); + + return c.json({ + name: group.name, + token: token, // Return unhashed token to user + }); +}); + +/** + * GET /api/ge-prices + * Get cached Grand Exchange prices + */ +publicRoutes.get('/ge-prices', async (c) => { + const now = Date.now(); + + // Refresh cache if expired + if (now - gePricesCacheTime > GE_CACHE_TTL) { + try { + const response = await fetch('https://prices.runescape.wiki/api/v1/osrs/latest'); + if (response.ok) { + const data = await response.json() as { data: Record }; + // Transform to simple id -> price mapping + gePricesCache = {}; + for (const [id, item] of Object.entries(data.data)) { + // Use high price if available, otherwise low + const price = item.high ?? item.low ?? 0; + gePricesCache[id] = price; + } + gePricesCacheTime = now; + } + } catch (error) { + console.error('Failed to fetch GE prices:', error); + } + } + + return c.json({ prices: gePricesCache }); +}); + +/** + * GET /api/captcha-enabled + * Get captcha configuration + */ +publicRoutes.get('/captcha-enabled', (c) => { + return c.json({ + enabled: CAPTCHA_ENABLED, + sitekey: CAPTCHA_SITEKEY, + }); +}); + +/** + * GET /api/collection-log-info + * Get collection log metadata + * TODO: Load from JSON file + */ +publicRoutes.get('/collection-log-info', (c) => { + // TODO: Load from collection_log_info.json + return c.json({}); +}); + +export default publicRoutes; diff --git a/server/src/utils/blake2.ts b/server/src/utils/blake2.ts new file mode 100644 index 00000000..51d37fb9 --- /dev/null +++ b/server/src/utils/blake2.ts @@ -0,0 +1,35 @@ +import blake2b from 'blakejs'; + +const BACKEND_SECRET = process.env.BACKEND_SECRET || 'changeme_secret_key_for_production'; + +/** + * Hash a token with Blake2b-256 (2 iterations). + * This must match the Spring/Rust implementation exactly: + * - Uses Blake2b-256 (32 bytes output) + * - 2 iterations of hashing + * - Combines token + secret + salt (group name) + * - Returns hex-encoded hash (64 characters) + */ +export function hashToken(token: string, salt: string): string { + // First iteration: hash(token + secret + salt) + const input1 = token + BACKEND_SECRET + salt; + const hash1 = blake2b.blake2b(input1, undefined, 32); + + // Second iteration: hash(hash1) + const hash2 = blake2b.blake2b(hash1, undefined, 32); + + // Return hex-encoded (lowercase) + return Buffer.from(hash2).toString('hex').toLowerCase(); +} + +/** + * Verify if a token matches the stored hash. + */ +export function verifyToken(token: string, salt: string, storedHash: string): boolean { + try { + const computedHash = hashToken(token, salt); + return computedHash === storedHash.toLowerCase(); + } catch { + return false; + } +} diff --git a/server/src/utils/password.ts b/server/src/utils/password.ts new file mode 100644 index 00000000..fb6da050 --- /dev/null +++ b/server/src/utils/password.ts @@ -0,0 +1,11 @@ +import bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 12; + +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, SALT_ROUNDS); +} + +export async function verifyPassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..1aa84da4 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}