diff --git a/API/Mainapi/.env.example b/API/Mainapi/.env.example index 54b2ac5..f947276 100644 --- a/API/Mainapi/.env.example +++ b/API/Mainapi/.env.example @@ -85,3 +85,13 @@ VIP_PAYBLIS_STORE_NAME=Movix # incoming request, and the checkout domain defaults to pay.payblis.com. VIP_PAYBLIS_IPN_BASE_URL= VIP_PAYBLIS_DOMAIN=pay.payblis.com + +# === Hydracker queue decoding === +# When deployed without frontend support for 202, set to 'false' to disable +# the queue (route returns 404 on cache miss instead of 202). Default: true. +HYDRACKER_QUEUE_ENABLED=true +# When 'false', /decode/:id bypasses Redis/queue entirely and POSTs hydracker +# inline (synchronous mode, like before the queue was introduced). The drain +# timer is also skipped. Useful in dev / low-traffic where the queue rarely +# reaches 50. Default: true (queue+batch of 50 enabled). +HYDRACKER_BATCHING_ENABLED=true diff --git a/API/Mainapi/README.md b/API/Mainapi/README.md index e63e65e..19ab697 100644 --- a/API/Mainapi/README.md +++ b/API/Mainapi/README.md @@ -65,7 +65,7 @@ Le fichier `API/Mainapi/.env.example` est la référence complète. En pratique, - cache et coordination : `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `NUM_WORKERS` - scraping / proxy : `PROXY_SERVER_URL`, `CF_PROXY_403_URL`, `BYPASS403_SERVER_URL`, `SOCKS5_PROXIES`, `HTTP_PROXIES` - anti-abuse / forms : `TURNSTILE_SECRET_KEY`, `TURNSTILE_INVISIBLE_SECRETKEY` -- paiement / VIP : variables `VIP_*`, `BTC_EXPLORER_API`, `LTC_EXPLORER_API` +- paiement / VIP : variables `VIP_*`, `BLOCKCYPHER_TOKEN` Certaines intégrations sont très spécifiques à des sources données, par exemple les cookies `DARKIWORLD_*`, `FSTREAM_LOGIN_*` ou `XTREAM_*`. diff --git a/API/Mainapi/app.js b/API/Mainapi/app.js index abc8789..e8f8df8 100644 --- a/API/Mainapi/app.js +++ b/API/Mainapi/app.js @@ -31,6 +31,8 @@ const { shouldUpdateCacheFrenchStream, shouldUpdateCacheLecteurVideo, shouldUpdateCache24h, + shouldUpdateCache48h, + generateCacheKey, CACHE_DIR, } = require("./utils/cacheManager"); @@ -64,6 +66,17 @@ const DARKIWORLD_BASE_URL = normalizeBaseUrl( const cookieJar = new tough.CookieJar(); // === Darkino session & headers setup === +// UA + client hints stables (Chrome/Brave 148 Windows). darkiworld a une +// limite de session par client, donc TOUTES les requêtes (refresh `/`, +// /api/v1/titles/.../content/liens, /api/v1/download-premium/..., +// seasons/episodes) doivent partager exactement ce fingerprint pour rester +// sur une seule session côté upstream. +// +// Cookies + x-xsrf-token : viennent de l'env (DARKIWORLD_COOKIES / +// DARKIWORLD_XSRF_TOKEN). cf_clearance volontairement absent de l'env : +// Cloudflare le renouvelle régulièrement, le set-cookie de réponse arrive +// dans le tough-cookie jar et `mergeCookieHeaders(jarState, configured)` +// ajoute les cookies du jar absents de la string env sans écraser ceux fixés. const darkiHeaders = { accept: "application/json", "accept-encoding": "gzip, deflate, br", @@ -72,18 +85,31 @@ const darkiHeaders = { cookie: process.env.DARKIWORLD_COOKIES || "", pragma: "no-cache", priority: "u=1, i", + "sec-ch-ua": + '"Chromium";v="148", "Brave";v="148", "Not/A)Brand";v="99"', + "sec-ch-ua-arch": '"x86"', + "sec-ch-ua-bitness": '"64"', + "sec-ch-ua-full-version-list": + '"Chromium";v="148.0.0.0", "Brave";v="148.0.0.0", "Not/A)Brand";v="99.0.0.0"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-model": '""', + "sec-ch-ua-platform": '"Windows"', + "sec-ch-ua-platform-version": '"19.0.0"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", + "sec-gpc": "1", + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", "x-xsrf-token": process.env.DARKIWORLD_XSRF_TOKEN || "", }; // Coflix config -const COFLIX_BASE_URL = "https://coflix.click"; +const COFLIX_BASE_URL = "https://coflix.date"; const coflixHeaders = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - Referer: "https://coflix.click", + Referer: "https://coflix.date", }; // === Axios instances for each source === @@ -132,16 +158,32 @@ const axiosFStream = axios.create({ decompress: true, }); -// Darkino session refresh -let lastDarkinoHomeRequest = 0; -const DARKINO_SESSION_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes +// Darkino session refresh — coordonné via Redis pour qu'un SEUL worker du +// cluster pinge l'upstream toutes les 10 minutes (et pas N workers en parallèle, +// ce qui ferait exploser la limite de session côté darkiworld). +const DARKINO_SESSION_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes +const DARKINO_REFRESH_LAST_KEY = "darkino:lastRefreshAt"; +const DARKINO_REFRESH_LOCK_KEY = "darkino:refreshLock"; +const DARKINO_REFRESH_LOCK_TTL_MS = 30 * 1000; // filet si le worker crash pendant le GET (axios timeout = 5s) const refreshDarkinoSessionIfNeeded = async () => { - const now = Date.now(); - if (now - lastDarkinoHomeRequest > DARKINO_SESSION_REFRESH_INTERVAL) { + try { + const last = Number(await redis.get(DARKINO_REFRESH_LAST_KEY)) || 0; + if (Date.now() - last <= DARKINO_SESSION_REFRESH_INTERVAL) return; + + // SET NX PX : seul le worker qui acquiert exécute le refresh, les autres no-op. + const acquired = await redis.set( + DARKINO_REFRESH_LOCK_KEY, + String(process.pid), + "PX", + DARKINO_REFRESH_LOCK_TTL_MS, + "NX", + ); + if (!acquired) return; + try { await axiosHelpers.axiosDarkinoRequest({ method: "get", url: "/" }); - lastDarkinoHomeRequest = now; + await redis.set(DARKINO_REFRESH_LAST_KEY, String(Date.now())); console.log("[DARKINO] Session refreshed"); } catch (error) { if ( @@ -150,7 +192,12 @@ const refreshDarkinoSessionIfNeeded = async () => { ) { console.error("[DARKINO] Failed to refresh session:", error.message); } + } finally { + await redis.del(DARKINO_REFRESH_LOCK_KEY).catch(() => {}); } + } catch (_) { + // Redis down → skip silently. Pas de fallback in-memory : ce serait + // réintroduire le bug N-workers-refreshent-en-parallèle. } }; @@ -213,6 +260,20 @@ app.use(jsonParseErrorHandler); app.use(express.urlencoded({ extended: true, limit: "5mb" })); // Reduced from 1000mb to prevent abuse +// 8. Serve uploaded OAuth app icons (`public/oauth-icons/`). +// Le panel admin upload ici, OAuthAuthorizePage lit `/oauth-icons/`. +const { ICON_DIR: OAUTH_ICON_DIR } = require('./utils/oauthClientsDb'); +app.use( + '/oauth-icons', + express.static(OAUTH_ICON_DIR, { + fallthrough: false, + maxAge: '7d', + setHeaders: (res) => { + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + }, + }), +); + // ========================================================================== // Configure route modules with dependencies from extracted utilities // ========================================================================== @@ -359,7 +420,9 @@ darkiworldRouter.configure({ saveToCache, shouldUpdateCache, shouldUpdateCache24h, + shouldUpdateCache48h, refreshDarkinoSessionIfNeeded, + redis, }); // --- Configure voirdrama --- @@ -410,6 +473,7 @@ app.use('/api/profiles', require('./routes/profiles')); app.use('/api/help', require('./routes/helpFeedback')); app.use('/api/auth', require('./routes/authRoutes')); app.use('/api/oauth', oauthRouter); +app.use('/api/admin/oauth-apps', require('./routes/adminOauthApps')); app.use('/api/sessions', require('./routes/sessions')); app.use('/api', require('./routes/debrid')); app.use('/proxy', require('./routes/proxy')); @@ -484,6 +548,16 @@ const appReady = (async () => { await ensureOAuthStorage(pool); console.log('OAuth tables initialized successfully'); + // OAuth client config (table oauth_clients + stats + grants VIP). + // ensureTables et migrateLegacyJsonIfNeeded sont protégés par le lock + // mais idempotents — sûr sur restart cluster. reloadCache hydrate + // le cache in-process de CE worker (chaque worker a le sien). + const oauthClientsDb = require('./utils/oauthClientsDb'); + await oauthClientsDb.ensureTables(); + await oauthClientsDb.migrateLegacyJsonIfNeeded(); + await oauthClientsDb.reloadCache(); + console.log('OAuth client tables initialized successfully'); + // Initialize Wishboard routes const { createWishboardRouter } = require("./wishboardRoutes"); const wishboardRouter = createWishboardRouter(pool, redis); @@ -589,6 +663,39 @@ function getAppPool() { return getPool(); } +// === Hydracker queue drain timer === +// Every worker fires the tick. drainQueueOnce uses Redis worker_lock so only +// one worker actually drains; the others get { drained: false, reason: 'lock_taken' } +// and skip. This means the drain runs in a worker process which has full +// darkiworld auth context (cookies, XSRF, darkiHeaders, axiosDarkinoRequest +// configured via axiosHelpers.configure earlier in this file). +const hydrackerQueue = require('./utils/hydrackerQueue'); +const DRAIN_INTERVAL_MS = 5000; +if (hydrackerQueue.BATCHING_ENABLED) { + setInterval(async () => { + try { + const result = await hydrackerQueue.drainQueueOnce({ + redis, + cacheDir: DOWNLOAD_CACHE_DIR, + generateCacheKey, + getFromCacheNoExpiration, + saveToCache, + axiosDarkinoRequest: axiosHelpers.axiosDarkinoRequest, + refreshDarkinoSessionIfNeeded + }); + if (result.drained) { + console.log(`[hydracker] drained batch of ${result.batchSize}`); + } else if (result.error) { + console.warn(`[hydracker] drain error: ${result.error} (requeued ${result.requeued || 0})`); + } + } catch (e) { + console.warn(`[hydracker] drain tick threw:`, e?.message || e); + } + }, DRAIN_INTERVAL_MS); +} else { + console.log('[hydracker] HYDRACKER_BATCHING_ENABLED=false → drain timer skipped, decode runs synchronously'); +} + // === Unified error handler === app.use((err, req, res, next) => { if (err.message !== "Not allowed by CORS") { diff --git a/API/Mainapi/commentsRoutes.js b/API/Mainapi/commentsRoutes.js index 6f3b59c..9c2720a 100644 --- a/API/Mainapi/commentsRoutes.js +++ b/API/Mainapi/commentsRoutes.js @@ -299,6 +299,77 @@ const requireAuth = async (req, res, next) => { } }; +// Rate limit pour les actions d'écriture (commentaires/réponses/réactions/notifications) +// - Clef = userId post-auth (fallback IP CF/X-Forwarded-For derrière Cloudflare) +// - Store Redis partagé entre workers du cluster (sinon chaque worker compte indépendamment) +// - passOnStoreError: si Redis tombe, fail-open au lieu de bloquer toutes les requêtes +const rateLimit = require("express-rate-limit"); +const { ipKeyGenerator } = require("express-rate-limit"); +const { createRedisRateLimitStore } = require("./utils/redisRateLimitStore"); +const writeRateLimit = rateLimit({ + windowMs: 60 * 1000, + max: 100, + store: createRedisRateLimitStore({ + prefix: "rate-limit:comments:write:", + windowMs: 60 * 1000, + }), + passOnStoreError: true, + standardHeaders: true, + legacyHeaders: false, + message: { error: "Trop de requêtes. Réessayez dans une minute." }, + keyGenerator: (req) => { + if (req.user) return `u:${req.user.userType}:${req.user.userId}`; + return ( + req.headers["cf-connecting-ip"] || + req.headers["x-forwarded-for"]?.split(",")[0].trim() || + ipKeyGenerator(req.ip) + ); + }, + validate: { + xForwardedForHeader: false, + ip: false, + keyGeneratorIpFallback: false, + }, +}); + +// Init paresseux des tables notifs/push (évite un DDL par requête, idempotent en cas de redémarrage) +let _notificationTablesInitialized = false; +let _notificationTablesInitPromise = null; +async function ensureNotificationTables() { + if (_notificationTablesInitialized) return; + if (_notificationTablesInitPromise) return _notificationTablesInitPromise; + _notificationTablesInitPromise = (async () => { + const pool = getCachedPool(); + await pool.execute( + `CREATE TABLE IF NOT EXISTS user_notification_preferences ( + user_id VARCHAR(255) NOT NULL, + user_type VARCHAR(50) NOT NULL, + notifications_disabled TINYINT(1) DEFAULT 0, + updated_at BIGINT, + PRIMARY KEY (user_id, user_type) + )` + ); + await pool.execute( + `CREATE TABLE IF NOT EXISTS push_subscriptions ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + user_type VARCHAR(50) NOT NULL, + endpoint TEXT NOT NULL, + p256dh TEXT NOT NULL, + auth TEXT NOT NULL, + created_at BIGINT, + INDEX idx_user_push (user_id, user_type) + )` + ); + _notificationTablesInitialized = true; + })(); + try { + await _notificationTablesInitPromise; + } finally { + _notificationTablesInitPromise = null; + } +} + // Helper to get allowed profile IDs (security check) async function getProfileIds(userId, userType) { try { @@ -1086,6 +1157,15 @@ router.get("/notifications", requireAuth, async (req, res) => { return res.status(400).json({ error: "profileId requis" }); } + // Vérifier que le profileId appartient bien à l'utilisateur authentifié + const userProfileIds = await getProfileIds( + req.user.userId, + req.user.userType, + ); + if (!userProfileIds.includes(profileId)) { + return res.status(403).json({ error: "Profil non autorisé" }); + } + let query = "SELECT * FROM notifications WHERE user_id = ? AND user_type = ? AND profile_id = ?"; const params = [req.user.userId, req.user.userType, profileId]; @@ -1107,7 +1187,7 @@ router.get("/notifications", requireAuth, async (req, res) => { }); // PUT /api/comments/notifications/:id/read - Marquer une notification comme lue -router.put("/notifications/:id/read", requireAuth, async (req, res) => { +router.put("/notifications/:id/read", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; const { profileId } = req.body; @@ -1117,6 +1197,15 @@ router.put("/notifications/:id/read", requireAuth, async (req, res) => { return res.status(400).json({ error: "profileId requis" }); } + // Vérifier que le profileId appartient bien à l'utilisateur authentifié + const userProfileIds = await getProfileIds( + req.user.userId, + req.user.userType, + ); + if (!userProfileIds.includes(profileId)) { + return res.status(403).json({ error: "Profil non autorisé" }); + } + await dbRun( "UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ? AND user_type = ? AND profile_id = ?", [id, req.user.userId, req.user.userType, profileId], @@ -1130,7 +1219,7 @@ router.put("/notifications/:id/read", requireAuth, async (req, res) => { }); // PUT /api/comments/notifications/read-all - Marquer toutes les notifications comme lues -router.put("/notifications/read-all", requireAuth, async (req, res) => { +router.put("/notifications/read-all", requireAuth, writeRateLimit, async (req, res) => { try { const { profileId } = req.body; @@ -1139,6 +1228,15 @@ router.put("/notifications/read-all", requireAuth, async (req, res) => { return res.status(400).json({ error: "profileId requis" }); } + // Vérifier que le profileId appartient bien à l'utilisateur authentifié + const userProfileIds = await getProfileIds( + req.user.userId, + req.user.userType, + ); + if (!userProfileIds.includes(profileId)) { + return res.status(403).json({ error: "Profil non autorisé" }); + } + await dbRun( "UPDATE notifications SET is_read = 1 WHERE user_id = ? AND user_type = ? AND profile_id = ?", [req.user.userId, req.user.userType, profileId], @@ -1154,7 +1252,7 @@ router.put("/notifications/read-all", requireAuth, async (req, res) => { }); // DELETE /api/comments/notifications/:id - Supprimer une notification -router.delete("/notifications/:id", requireAuth, async (req, res) => { +router.delete("/notifications/:id", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; const { profileId } = req.query; @@ -1164,6 +1262,15 @@ router.delete("/notifications/:id", requireAuth, async (req, res) => { return res.status(400).json({ error: "profileId requis" }); } + // Vérifier que le profileId appartient bien à l'utilisateur authentifié + const userProfileIds = await getProfileIds( + req.user.userId, + req.user.userType, + ); + if (!userProfileIds.includes(profileId)) { + return res.status(403).json({ error: "Profil non autorisé" }); + } + // Vérifier que la notification appartient à l'utilisateur et au profil const notification = await dbGet( "SELECT * FROM notifications WHERE id = ? AND user_id = ? AND user_type = ? AND profile_id = ?", @@ -1190,17 +1297,8 @@ router.delete("/notifications/:id", requireAuth, async (req, res) => { // GET /api/comments/notifications/preferences - Récupérer les préférences de notifications router.get("/notifications/preferences", requireAuth, async (req, res) => { try { - const pool = getPool(); - await pool.execute( - `CREATE TABLE IF NOT EXISTS user_notification_preferences ( - user_id VARCHAR(255) NOT NULL, - user_type VARCHAR(50) NOT NULL, - notifications_disabled TINYINT(1) DEFAULT 0, - updated_at BIGINT, - PRIMARY KEY (user_id, user_type) - )` - ); - + await ensureNotificationTables(); + const pool = getCachedPool(); const [rows] = await pool.execute( 'SELECT notifications_disabled FROM user_notification_preferences WHERE user_id = ? AND user_type = ? LIMIT 1', [req.user.userId, req.user.userType] @@ -1217,21 +1315,11 @@ router.get("/notifications/preferences", requireAuth, async (req, res) => { }); // PUT /api/comments/notifications/preferences - Mettre à jour les préférences de notifications -router.put("/notifications/preferences", requireAuth, async (req, res) => { +router.put("/notifications/preferences", requireAuth, writeRateLimit, async (req, res) => { try { + await ensureNotificationTables(); const disabled = req.body?.notificationsDisabled === true; - const pool = getPool(); - - await pool.execute( - `CREATE TABLE IF NOT EXISTS user_notification_preferences ( - user_id VARCHAR(255) NOT NULL, - user_type VARCHAR(50) NOT NULL, - notifications_disabled TINYINT(1) DEFAULT 0, - updated_at BIGINT, - PRIMARY KEY (user_id, user_type) - )` - ); - + const pool = getCachedPool(); await pool.execute( `INSERT INTO user_notification_preferences (user_id, user_type, notifications_disabled, updated_at) VALUES (?, ?, ?, ?) @@ -1247,31 +1335,68 @@ router.put("/notifications/preferences", requireAuth, async (req, res) => { }); // POST /api/comments/notifications/push/subscribe - Enregistrer une subscription push -router.post("/notifications/push/subscribe", requireAuth, async (req, res) => { +router.post("/notifications/push/subscribe", requireAuth, writeRateLimit, async (req, res) => { try { const { subscription } = req.body; - if (!subscription || !subscription.endpoint) { + // Validation stricte: endpoint + keys.p256dh + keys.auth requis + if ( + !subscription || + typeof subscription.endpoint !== "string" || + !subscription.endpoint || + !subscription.keys || + typeof subscription.keys.p256dh !== "string" || + !subscription.keys.p256dh || + typeof subscription.keys.auth !== "string" || + !subscription.keys.auth + ) { return res.status(400).json({ error: "Subscription invalide" }); } - const pool = getPool(); - await pool.execute( - `CREATE TABLE IF NOT EXISTS push_subscriptions ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - user_type VARCHAR(50) NOT NULL, - endpoint TEXT NOT NULL, - p256dh TEXT NOT NULL, - auth TEXT NOT NULL, - created_at BIGINT, - INDEX idx_user_push (user_id, user_type) - )` - ); - // Supprimer les anciennes subscriptions du même endpoint - await pool.execute('DELETE FROM push_subscriptions WHERE endpoint = ?', [subscription.endpoint]); - await pool.execute( - 'INSERT INTO push_subscriptions (user_id, user_type, endpoint, p256dh, auth, created_at) VALUES (?, ?, ?, ?, ?, ?)', - [req.user.userId, req.user.userType, subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth, Date.now()] + + await ensureNotificationTables(); + const pool = getCachedPool(); + + // Vérifier si l'endpoint existe déjà — refuser le hijack cross-user + const [existing] = await pool.execute( + 'SELECT user_id, user_type FROM push_subscriptions WHERE endpoint = ? LIMIT 1', + [subscription.endpoint] ); + + if (existing.length > 0) { + const owner = existing[0]; + if ( + String(owner.user_id) !== String(req.user.userId) || + String(owner.user_type) !== String(req.user.userType) + ) { + // Endpoint appartient à un autre compte — refuser (anti-hijack push) + return res + .status(409) + .json({ error: "Endpoint déjà associé à un autre compte" }); + } + // Même owner: rotate les clés (cas normal, le browser peut renouveler les clés) + await pool.execute( + 'UPDATE push_subscriptions SET p256dh = ?, auth = ?, created_at = ? WHERE endpoint = ? AND user_id = ? AND user_type = ?', + [ + subscription.keys.p256dh, + subscription.keys.auth, + Date.now(), + subscription.endpoint, + req.user.userId, + req.user.userType, + ] + ); + } else { + await pool.execute( + 'INSERT INTO push_subscriptions (user_id, user_type, endpoint, p256dh, auth, created_at) VALUES (?, ?, ?, ?, ?, ?)', + [ + req.user.userId, + req.user.userType, + subscription.endpoint, + subscription.keys.p256dh, + subscription.keys.auth, + Date.now(), + ] + ); + } res.json({ success: true }); } catch (error) { console.error("Erreur lors de l'enregistrement push:", error); @@ -1280,11 +1405,12 @@ router.post("/notifications/push/subscribe", requireAuth, async (req, res) => { }); // DELETE /api/comments/notifications/push/unsubscribe - Supprimer une subscription push -router.delete("/notifications/push/unsubscribe", requireAuth, async (req, res) => { +router.delete("/notifications/push/unsubscribe", requireAuth, writeRateLimit, async (req, res) => { try { const { endpoint } = req.body; if (!endpoint) return res.status(400).json({ error: "Endpoint manquant" }); - const pool = getPool(); + await ensureNotificationTables(); + const pool = getCachedPool(); await pool.execute('DELETE FROM push_subscriptions WHERE endpoint = ? AND user_id = ? AND user_type = ?', [endpoint, req.user.userId, req.user.userType]); res.json({ success: true }); } catch (error) { @@ -1301,10 +1427,15 @@ router.get("/notifications/push/vapid-key", (req, res) => { // ==================== ROUTES RÉACTIONS ==================== // POST /api/comments/react - Ajouter/retirer une réaction -router.post("/react", requireAuth, async (req, res) => { +router.post("/react", requireAuth, writeRateLimit, async (req, res) => { try { const { targetType, targetId, profileId } = req.body; // targetType: 'comment' ou 'reply' + // Whitelist targetType pour éviter pollution de la table comment_reactions + if (!["comment", "reply"].includes(targetType)) { + return res.status(400).json({ error: "targetType invalide" }); + } + // Verify profile ownership const userProfileIds = await getProfileIds( req.user.userId, @@ -1448,6 +1579,11 @@ router.get( const { targetType, targetId } = req.params; const { profileId } = req.query; + // Whitelist targetType + if (!["comment", "reply"].includes(targetType)) { + return res.status(400).json({ error: "targetType invalide" }); + } + const reaction = await dbGet( "SELECT * FROM comment_reactions WHERE target_type = ? AND target_id = ? AND user_id = ? AND user_type = ? AND profile_id = ?", [targetType, targetId, req.user.userId, req.user.userType, profileId], @@ -1468,8 +1604,9 @@ router.get( router.get("/:commentId/replies", async (req, res) => { try { const { commentId } = req.params; - const { page = 1, limit = 3 } = req.query; - const offset = (page - 1) * limit; + const safePage = Math.max(1, Math.min(parseInt(req.query.page) || 1, 1000)); + const safeLimit = Math.max(1, Math.min(parseInt(req.query.limit) || 3, 50)); + const offset = (safePage - 1) * safeLimit; // Tenter de récupérer l'utilisateur connecté (optionnel) let currentUser = null; @@ -1516,8 +1653,8 @@ router.get("/:commentId/replies", async (req, res) => { currentUser.userType, profileId, commentId, - parseInt(limit), - parseInt(offset), + safeLimit, + offset, ]; } else { repliesQuery = ` @@ -1527,7 +1664,7 @@ router.get("/:commentId/replies", async (req, res) => { WHERE cr.comment_id = ? AND cr.deleted = 0 ORDER BY cr.hierarchical_path ASC LIMIT ? OFFSET ?`; - repliesParams = [commentId, parseInt(limit), parseInt(offset)]; + repliesParams = [commentId, safeLimit, offset]; } const replies = await dbAll(repliesQuery, repliesParams); @@ -1565,8 +1702,8 @@ router.get("/:commentId/replies", async (req, res) => { res.json({ replies: repliesWithDetails, total: totalResult.total, - page: parseInt(page), - limit: parseInt(limit), + page: safePage, + limit: safeLimit, hasMore: offset + repliesWithDetails.length < totalResult.total, }); } catch (error) { @@ -1576,7 +1713,7 @@ router.get("/:commentId/replies", async (req, res) => { }); // POST /api/comments/:commentId/replies - Créer une réponse -router.post("/:commentId/replies", requireAuth, async (req, res) => { +router.post("/:commentId/replies", requireAuth, writeRateLimit, async (req, res) => { try { const { commentId } = req.params; let { @@ -1827,10 +1964,10 @@ router.post("/:commentId/replies", requireAuth, async (req, res) => { }); // PUT /api/comments/replies/:id - Éditer une réponse -router.put("/replies/:id", requireAuth, async (req, res) => { +router.put("/replies/:id", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; - let { content, isSpoiler } = req.body; + let { content, isSpoiler, profileId } = req.body; // Normalize content while preserving the original characters content = normalizeCommentContent(content); @@ -1857,6 +1994,16 @@ router.put("/replies/:id", requireAuth, async (req, res) => { return res.status(403).json({ error: "Non autorisé" }); } + // Si la réponse a un profile_id, exiger que l'éditeur passe le même profileId + // (empêche un autre profil du même compte d'éditer) + if (reply.profile_id) { + if (!profileId || String(reply.profile_id) !== String(profileId)) { + return res + .status(403) + .json({ error: "Seul le profil auteur peut éditer cette réponse" }); + } + } + // Mettre à jour la réponse await dbRun( "UPDATE comment_replies SET content = ?, is_spoiler = ?, is_edited = 1, updated_at = ? WHERE id = ?", @@ -1880,7 +2027,7 @@ router.put("/replies/:id", requireAuth, async (req, res) => { }); // DELETE /api/comments/replies/:id - Supprimer une réponse (admin ou auteur) -router.delete("/replies/:id", requireAuth, async (req, res) => { +router.delete("/replies/:id", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; const { profileId } = req.query; @@ -1899,10 +2046,13 @@ router.delete("/replies/:id", requireAuth, async (req, res) => { const userMatch = String(reply.user_id) === String(req.user.userId) && String(reply.user_type) === String(req.user.userType); - const profileMatch = - !reply.profile_id || - !profileId || - String(reply.profile_id) === String(profileId); + // Si la réponse a un profile_id, le profileId fourni doit matcher exactement + // (empêche un kid profile de supprimer la réponse d'un adult profile du même compte) + let profileMatch = true; + if (reply.profile_id) { + profileMatch = + !!profileId && String(reply.profile_id) === String(profileId); + } const isOwner = userMatch && profileMatch; if (!userData.isAdmin && !isOwner) { @@ -2927,18 +3077,21 @@ router.get("/limits", requireAuth, async (req, res) => { // ==================== ROUTES REPORTS (avant les routes dynamiques) ==================== -const rateLimit = require("express-rate-limit"); - const reportRateLimit = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, + store: createRedisRateLimitStore({ + prefix: "rate-limit:comments:report:", + windowMs: 15 * 60 * 1000, + }), + passOnStoreError: true, standardHeaders: true, legacyHeaders: false, message: { error: "Trop de signalements. Réessayez dans 15 minutes." }, keyGenerator: (req) => req.headers["cf-connecting-ip"] || req.headers["x-forwarded-for"]?.split(",")[0].trim() || - req.ip, + ipKeyGenerator(req.ip), validate: { xForwardedForHeader: false, ip: false, @@ -3364,8 +3517,9 @@ router.put("/admin/reports/:id/dismiss", requireAuth, async (req, res) => { router.get("/:contentType/:contentId", async (req, res) => { try { const { contentType, contentId } = req.params; - const { page = 1, limit = 20 } = req.query; - const offset = (page - 1) * limit; + const safePage = Math.max(1, Math.min(parseInt(req.query.page) || 1, 1000)); + const safeLimit = Math.max(1, Math.min(parseInt(req.query.limit) || 20, 50)); + const offset = (safePage - 1) * safeLimit; // Tenter de récupérer l'utilisateur connecté (optionnel) let currentUser = null; @@ -3414,8 +3568,8 @@ router.get("/:contentType/:contentId", async (req, res) => { profileId, contentType, contentId, - parseInt(limit), - parseInt(offset), + safeLimit, + offset, ]; } else { commentsQuery = ` @@ -3429,8 +3583,8 @@ router.get("/:contentType/:contentId", async (req, res) => { commentsParams = [ contentType, contentId, - parseInt(limit), - parseInt(offset), + safeLimit, + offset, ]; } @@ -3470,8 +3624,8 @@ router.get("/:contentType/:contentId", async (req, res) => { res.json({ comments: commentsWithDetails, total: totalResult.total, - page: parseInt(page), - limit: parseInt(limit), + page: safePage, + limit: safeLimit, hasMore: offset + commentsWithDetails.length < totalResult.total, }); } catch (error) { @@ -3481,8 +3635,26 @@ router.get("/:contentType/:contentId", async (req, res) => { }); // POST /api/comments - Créer un commentaire -router.post("/", requireAuth, async (req, res) => { +router.post("/", requireAuth, writeRateLimit, async (req, res) => { + // Lock Redis par user pour sérialiser les créations concurrentes + // (évite la race count >=3 / count >=10 entre SELECT et INSERT) + const userLockKey = `comments:create:lock:${req.user.userType}:${req.user.userId}`; + let lockAcquired = null; + let lockHeld = false; try { + try { + lockAcquired = await redis.set(userLockKey, "1", "EX", 10, "NX"); + } catch { + // Redis indisponible — on accepte le risque de race (best effort) + lockAcquired = "OK"; + } + if (!lockAcquired) { + return res + .status(429) + .json({ error: "Une création est déjà en cours, réessayez." }); + } + lockHeld = true; + let { contentType, contentId, @@ -3640,14 +3812,22 @@ router.post("/", requireAuth, async (req, res) => { } catch (error) { console.error("Erreur lors de la création du commentaire:", error); res.status(500).json({ error: "Erreur serveur" }); + } finally { + if (lockHeld) { + try { + await redis.del(userLockKey); + } catch { + /* Redis indisponible — le lock expirera via TTL */ + } + } } }); // PUT /api/comments/:id - Éditer un commentaire -router.put("/:id", requireAuth, async (req, res) => { +router.put("/:id", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; - let { content, isSpoiler } = req.body; + let { content, isSpoiler, profileId } = req.body; // Normalize content while preserving the original characters content = normalizeCommentContent(content); @@ -3674,6 +3854,16 @@ router.put("/:id", requireAuth, async (req, res) => { return res.status(403).json({ error: "Non autorisé" }); } + // Si le commentaire a un profile_id, exiger que l'éditeur passe le même profileId + // (empêche un autre profil du même compte d'éditer) + if (comment.profile_id) { + if (!profileId || String(comment.profile_id) !== String(profileId)) { + return res + .status(403) + .json({ error: "Seul le profil auteur peut éditer ce commentaire" }); + } + } + // Mettre à jour le commentaire await dbRun( "UPDATE comments SET content = ?, is_spoiler = ?, is_edited = 1, updated_at = ? WHERE id = ?", @@ -3696,7 +3886,7 @@ router.put("/:id", requireAuth, async (req, res) => { }); // DELETE /api/comments/:id - Supprimer un commentaire (admin ou auteur) -router.delete("/:id", requireAuth, async (req, res) => { +router.delete("/:id", requireAuth, writeRateLimit, async (req, res) => { try { const { id } = req.params; const { profileId } = req.query; @@ -3715,11 +3905,13 @@ router.delete("/:id", requireAuth, async (req, res) => { const userMatch = String(comment.user_id) === String(req.user.userId) && String(comment.user_type) === String(req.user.userType); - // Vérifier le profile_id si le commentaire en a un - const profileMatch = - !comment.profile_id || - !profileId || - String(comment.profile_id) === String(profileId); + // Si le commentaire a un profile_id, le profileId fourni doit matcher exactement + // (empêche un kid profile de supprimer le commentaire d'un adult profile du même compte) + let profileMatch = true; + if (comment.profile_id) { + profileMatch = + !!profileId && String(comment.profile_id) === String(profileId); + } const isOwner = userMatch && profileMatch; if (!userData.isAdmin && !isOwner) { diff --git a/API/Mainapi/exportscripts/add_oauth_apps_tables.sql b/API/Mainapi/exportscripts/add_oauth_apps_tables.sql new file mode 100644 index 0000000..80239ce --- /dev/null +++ b/API/Mainapi/exportscripts/add_oauth_apps_tables.sql @@ -0,0 +1,112 @@ +-- Migration : passage du fichier `data/oauth-clients.json` à 3 tables MySQL. +-- - oauth_clients : config des apps (remplace le JSON) +-- - oauth_app_stats : compteur d'appels par app + type d'event +-- - oauth_vip_grants : historique des grants VIP émis par chaque app +-- +-- Idempotent grâce à `CREATE TABLE IF NOT EXISTS`. +-- Lance avec : `mysql -u -p movix < add_oauth_apps_tables.sql` +-- ou via le script `routes/admin.js` au démarrage (auto-migrate). + +CREATE TABLE IF NOT EXISTS oauth_clients ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + -- Identifiant public visible dans le query OAuth (?client_id=...). + client_id VARCHAR(128) NOT NULL UNIQUE, + + -- Nom affiché sur la page d'autorisation et dans le panel admin. + client_name VARCHAR(200) NOT NULL, + + description TEXT NULL, + homepage_url VARCHAR(500) NULL, + + -- JSON arrays — sérialisation gérée côté Node. + redirect_uris JSON NOT NULL, + allowed_scopes JSON NOT NULL, + + -- Type de client : public (PKCE obligatoire, pas de secret) ou + -- confidentiel (client_secret nécessaire). + public_client TINYINT(1) NOT NULL DEFAULT 1, + require_pkce TINYINT(1) NOT NULL DEFAULT 1, + -- Secret en clair (uniquement si publicClient = 0). + client_secret VARCHAR(256) NULL, + + -- Nom de fichier de l'icône (relatif à `public/oauth-icons/`). + -- Ex : "movix-mcp-1234567890.png". NULL = pas d'icône custom. + icon_filename VARCHAR(200) NULL, + + -- Compteur de jours VIP que l'app peut distribuer via /api/oauth/vip/grant. + -- Décrément à chaque grant ; admin peut alimenter via le panel. + vip_days_balance INT NOT NULL DEFAULT 0, + + -- Désactivation soft (cache l'app de la list mais garde l'historique). + is_active TINYINT(1) NOT NULL DEFAULT 1, + + created_at BIGINT UNSIGNED NOT NULL, + updated_at BIGINT UNSIGNED NOT NULL, + + PRIMARY KEY (id), + KEY idx_client_id (client_id), + KEY idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Stats : un event = une ligne. Permet de grapher l'usage par app. +-- Cleanup périodique : `DELETE FROM oauth_app_stats WHERE created_at < (now - 90j)`. +CREATE TABLE IF NOT EXISTS oauth_app_stats ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + -- Référence vers oauth_clients.client_id (pas la PK numérique, pour + -- survivre à une suppression). + client_id VARCHAR(128) NOT NULL, + + -- Type d'event : 'authorize' (page d'auth affichée), 'authorize_granted' + -- (user a cliqué Autoriser), 'authorize_denied', 'token' (échange code → + -- token), 'api_call' (toute requête OAuth authentifiée), 'vip_grant'. + event_type VARCHAR(32) NOT NULL, + + -- User concerné (si applicable). Format `userType:userId`. + user_id VARCHAR(160) NULL, + + -- Métadonnées libres (path, status, scope demandé, etc.) en JSON. + metadata JSON NULL, + + created_at BIGINT UNSIGNED NOT NULL, + + PRIMARY KEY (id), + KEY idx_client_event (client_id, event_type, created_at), + KEY idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Historique des grants VIP. Chaque ligne = un grant fait par une app +-- à un user. Sert d'audit + sert à recréer une access_key si l'user +-- perd la sienne. +CREATE TABLE IF NOT EXISTS oauth_vip_grants ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + client_id VARCHAR(128) NOT NULL, + + -- User qui reçoit le VIP. Format `userType:userId`. + user_id VARCHAR(160) NOT NULL, + user_type VARCHAR(16) NOT NULL, + user_id_only VARCHAR(128) NOT NULL, + + -- Jours grantés (décrémenté de oauth_clients.vip_days_balance). + days_granted INT UNSIGNED NOT NULL, + + -- Access key générée (référence vers access_keys.key_value). + access_key_value VARCHAR(128) NOT NULL, + + -- Date de validité de la clé + expires_at DATETIME NOT NULL, + + -- Audit + granted_at BIGINT UNSIGNED NOT NULL, + + -- Si l'admin révoque le grant : on flag (mais on n'efface pas la clé + -- automatiquement — l'admin doit le faire séparément). + revoked_at BIGINT UNSIGNED NULL, + + PRIMARY KEY (id), + KEY idx_client_id (client_id, granted_at), + KEY idx_user_id (user_id, granted_at), + KEY idx_access_key (access_key_value) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/API/Mainapi/liveTvRoutes.js b/API/Mainapi/liveTvRoutes.js index 6f88a9f..e715542 100644 --- a/API/Mainapi/liveTvRoutes.js +++ b/API/Mainapi/liveTvRoutes.js @@ -95,7 +95,7 @@ const WITV_CATEGORIES = { }; // URL de base pour Sosplay -const SOSPLAY_BASE_URL = "https://ligue1live.xyz"; +const SOSPLAY_BASE_URL = "https://streamonsport.art"; // Source Bolaloca/Elitegol (remplace l'ancien catalogue Sosplay pour les chaines) const BOLALOCA_BASE_URL = "https://bolaloca.my"; @@ -265,7 +265,7 @@ const BOLALOCA_CHANNELS = [ }, ]; -const LIVETV_BASE_URL = "https://livetv876.me"; +const LIVETV_BASE_URL = "https://livetv882.me"; const LIVETV_EMBED_REFERER = `${LIVETV_BASE_URL}/`; const LIVETV_ALLUPCOMING_PATHS = ["/frx/allupcoming/", "/frx/ads/"]; const LIVETV_CATEGORIES = { @@ -1958,7 +1958,7 @@ function shouldIgnoreLiveTvIframeUrl(rawUrl) { const combined = `${hostname}${pathname}${search}`; if ( - hostname === "ads.livetv876.me" || + hostname === "ads.livetv882.me" || hostname.startsWith("ads.") || hostname.startsWith("ad.") ) { @@ -3571,7 +3571,6 @@ router.get("/manifest", async (req, res) => { manifest.catalogs = [...matchesCatalogs, ...manifest.catalogs]; manifest.idPrefixes.push("match_"); - // [DÉSACTIVÉ] Linkzy temporairement désactivé // for (const [catId, config] of Object.entries(LINKZY_CATEGORIES)) { // manifest.catalogs.push({ @@ -3762,8 +3761,7 @@ router.get("/stream/:type/:channelId", async (req, res) => { // Block VIP-only sources immediately if not VIP if ( - (channelId.startsWith("matches_") || - channelId.startsWith("iptv_")) && + (channelId.startsWith("matches_") || channelId.startsWith("iptv_")) && !isVip.vip ) { return res.status(403).json({ error: "Réservé aux membres VIP" }); @@ -4328,8 +4326,8 @@ router.delete("/cache", async (req, res) => { const XTREAM_URL = (process.env.XTREAM_URL || "").replace(/\/+$/, ""); const XTREAM_USER = process.env.XTREAM_USER || ""; const XTREAM_PASS = process.env.XTREAM_PASS || ""; -const IPTV_IMAGE_PROXY = "https://proxy.movix.blog/proxy"; -const IPTV_STREAM_PROXY = "https://proxiesembed.movix.blog/proxy"; +const IPTV_IMAGE_PROXY = "https://proxy.movix.tax/proxy"; +const IPTV_STREAM_PROXY = "https://proxiesembed.movix.tax/proxy"; // Cache catégories IPTV en mémoire (change rarement) let iptvCategoriesCache = null; @@ -4390,7 +4388,7 @@ router.get("/iptv/categories", requireVip, async (req, res) => { /** * GET /api/livetv/iptv/streams/:categoryId * Récupère les chaînes d'une catégorie Xtream (VIP only) - * Images proxifiées via proxy.movix.blog + * Images proxifiées via proxy.movix.cash */ router.get("/iptv/streams/:categoryId", requireVip, async (req, res) => { const { categoryId } = req.params; diff --git a/API/Mainapi/middleware/cors.js b/API/Mainapi/middleware/cors.js index fe5578f..675a200 100644 --- a/API/Mainapi/middleware/cors.js +++ b/API/Mainapi/middleware/cors.js @@ -7,6 +7,8 @@ const cors = require("cors"); const { getOAuthAllowedCorsOrigins } = require('../utils/oauthClients'); const STATIC_ALLOWED_DOMAINS = [ + 'movix.tax', + 'movix.cash', 'movix.blog', 'movix.rodeo', 'movix.club', diff --git a/API/Mainapi/middleware/security.js b/API/Mainapi/middleware/security.js index 459e41e..803f223 100644 --- a/API/Mainapi/middleware/security.js +++ b/API/Mainapi/middleware/security.js @@ -40,6 +40,8 @@ function domainRestriction(req, res, next) { const allowedDomains = [ 'localhost:3000', + 'movix.tax', + 'movix.cash', 'movix.blog', 'movix.rodeo', 'movix.club', diff --git a/API/Mainapi/public/oauth-icons/movix-mcp-1778761296022.jpg b/API/Mainapi/public/oauth-icons/movix-mcp-1778761296022.jpg new file mode 100644 index 0000000..106666f Binary files /dev/null and b/API/Mainapi/public/oauth-icons/movix-mcp-1778761296022.jpg differ diff --git a/API/Mainapi/routes/adminOauthApps.js b/API/Mainapi/routes/adminOauthApps.js new file mode 100644 index 0000000..1e0cb88 --- /dev/null +++ b/API/Mainapi/routes/adminOauthApps.js @@ -0,0 +1,604 @@ +/** + * Routes admin pour gérer les applications OAuth Movix. + * Mount : `app.use('/api/admin/oauth-apps', adminOauthAppsRouter)`. + * + * Toutes les routes sont protégées par `isAdmin` (table `admins`). + * Source de vérité : la table `oauth_clients` (alimentée au boot par + * `oauthClientsDb.reloadCache()`). Toute mutation appelle `reloadCache()` + * en fin de requête pour rafraîchir le cache du worker courant. + * + * Note multi-worker : chaque worker a son propre cache in-process. Une + * mutation depuis le worker A ne rafraîchit pas le cache du worker B + * immédiatement. C'est acceptable car : + * 1) les opérations admin sont rares ; + * 2) le cache est rechargé au boot ; + * 3) une lecture stale max 1 requête. + * Si besoin d'invalidation cross-worker → publier un message Redis. + */ + +const express = require('express'); +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); +const fsp = require('fs').promises; +const rateLimit = require('express-rate-limit'); +const { ipKeyGenerator } = require('express-rate-limit'); + +const { isAdmin } = require('../middleware/auth'); +const { getPool } = require('../mysqlPool'); +const oauthClientsDb = require('../utils/oauthClientsDb'); +const { KNOWN_OAUTH_SCOPES } = require('../utils/oauthClients'); +const { createRedisRateLimitStore } = require('../utils/redisRateLimitStore'); + +const router = express.Router(); + +const ALLOWED_ICON_MIME = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/webp': 'webp', +}; +const MAX_ICON_SIZE_BYTES = 256 * 1024; // 256 KB + +const CLIENT_ID_RE = /^[a-z0-9][a-z0-9-]{1,64}$/; + +// Petit rate-limiter pour les routes admin OAuth (anti-bruteforce sur les secrets). +const adminOauthAppsLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 60, + store: createRedisRateLimitStore({ prefix: 'rate-limit:admin:oauth-apps:' }), + passOnStoreError: true, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => + req.headers['cf-connecting-ip'] + || req.headers['x-forwarded-for']?.split(',')[0].trim() + || ipKeyGenerator(req.ip), + validate: { xForwardedForHeader: false, ip: false }, +}); + +router.use(adminOauthAppsLimiter); +router.use(isAdmin); + +// ─── helpers ──────────────────────────────────────────────────────────── + +function badRequest(res, message) { + return res.status(400).json({ success: false, error: message }); +} + +function notFound(res, message = 'Application OAuth introuvable') { + return res.status(404).json({ success: false, error: message }); +} + +function serverError(res, error, message = 'Erreur serveur') { + console.error('[adminOauthApps]', message, error?.message || error); + return res.status(500).json({ success: false, error: message }); +} + +function sanitizeClientId(raw) { + const value = String(raw || '').trim().toLowerCase(); + return CLIENT_ID_RE.test(value) ? value : null; +} + +function sanitizeClientName(raw) { + if (typeof raw !== 'string') return null; + const value = raw.trim().slice(0, 200); + return value.length >= 2 ? value : null; +} + +function sanitizeDescription(raw) { + if (raw == null || raw === '') return null; + if (typeof raw !== 'string') return null; + return raw.trim().slice(0, 2000) || null; +} + +function sanitizeHttpUrl(raw) { + if (raw == null || raw === '') return null; + if (typeof raw !== 'string') return null; + try { + const url = new URL(raw.trim()); + // HTTPS only : le homepageUrl est rendu en lien cliquable sur la page + // d'autorisation OAuth (boundary de confiance pour l'utilisateur). + // Pas d'exception loopback ici — c'est pour le marketing, pas pour OAuth. + if (url.protocol !== 'https:') return null; + url.hash = ''; + return url.toString(); + } catch { + return null; + } +} + +function sanitizeRedirectUris(rawArray) { + if (!Array.isArray(rawArray)) return null; + const result = []; + for (const raw of rawArray) { + if (typeof raw !== 'string') continue; + try { + const url = new URL(raw.trim()); + const host = url.hostname.toLowerCase(); + const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.localhost'); + if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) { + continue; + } + url.hash = ''; + result.push(url.toString()); + } catch { /* skip */ } + } + return result.length > 0 ? Array.from(new Set(result)) : null; +} + +function sanitizeScopes(rawArray) { + if (!Array.isArray(rawArray)) return null; + const result = Array.from(new Set( + rawArray + .map((s) => String(s || '').trim()) + .filter((s) => KNOWN_OAUTH_SCOPES.includes(s)), + )); + return result.length > 0 ? result : null; +} + +function generateClientSecret() { + // 64 chars hex = 256 bits — assez pour un secret OAuth. + return crypto.randomBytes(32).toString('hex'); +} + +function serializeAppRow(row) { + return { + id: Number(row.id), + clientId: row.client_id, + clientName: row.client_name, + description: row.description || null, + homepageUrl: row.homepage_url || null, + redirectUris: safeJsonParse(row.redirect_uris, []), + allowedScopes: safeJsonParse(row.allowed_scopes, []), + publicClient: row.public_client === 1 || row.public_client === true, + requirePkce: row.require_pkce === 1 || row.require_pkce === true, + hasClientSecret: !!row.client_secret, + iconFilename: row.icon_filename || null, + iconUrl: row.icon_filename ? `/oauth-icons/${row.icon_filename}` : null, + vipDaysBalance: Number(row.vip_days_balance || 0), + isActive: row.is_active === 1 || row.is_active === true, + createdAt: Number(row.created_at || 0), + updatedAt: Number(row.updated_at || 0), + }; +} + +function safeJsonParse(raw, fallback) { + if (typeof raw !== 'string' || !raw.trim()) { + return Array.isArray(raw) ? raw : fallback; + } + try { + return JSON.parse(raw); + } catch { + return fallback; + } +} + +async function fetchAppByClientId(pool, clientId) { + const [rows] = await pool.execute( + 'SELECT * FROM oauth_clients WHERE client_id = ? LIMIT 1', + [clientId], + ); + return rows[0] || null; +} + +async function removeIconFile(filename) { + if (!filename) return; + const target = path.join(oauthClientsDb.ICON_DIR, path.basename(filename)); + try { + await fsp.unlink(target); + } catch { + /* swallow: déjà absent */ + } +} + +// ─── routes ───────────────────────────────────────────────────────────── + +router.get('/scopes', (req, res) => { + res.json({ success: true, scopes: [...KNOWN_OAUTH_SCOPES] }); +}); + +router.get('/', async (req, res) => { + try { + const pool = getPool(); + const includeInactive = req.query?.inactive === '1' || req.query?.inactive === 'true'; + const whereSql = includeInactive ? '' : 'WHERE is_active = 1'; + const [rows] = await pool.execute( + `SELECT * FROM oauth_clients ${whereSql} ORDER BY created_at DESC`, + ); + + // Stats compactes par app (30 derniers jours) pour l'affichage en liste. + const since = Date.now() - 30 * 24 * 60 * 60 * 1000; + const [statsRows] = await pool.execute( + `SELECT client_id, event_type, COUNT(*) AS n + FROM oauth_app_stats + WHERE created_at >= ? + GROUP BY client_id, event_type`, + [since], + ); + const statsByClient = new Map(); + for (const r of statsRows) { + if (!statsByClient.has(r.client_id)) statsByClient.set(r.client_id, {}); + statsByClient.get(r.client_id)[r.event_type] = Number(r.n); + } + + const apps = rows.map((row) => ({ + ...serializeAppRow(row), + stats30d: statsByClient.get(row.client_id) || {}, + })); + return res.json({ success: true, apps }); + } catch (err) { + return serverError(res, err, 'Impossible de lister les applications'); + } +}); + +router.get('/:clientId', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const pool = getPool(); + const row = await fetchAppByClientId(pool, clientId); + if (!row) return notFound(res); + return res.json({ success: true, app: serializeAppRow(row) }); + } catch (err) { + return serverError(res, err, 'Impossible de charger l\'application'); + } +}); + +router.post('/', async (req, res) => { + try { + const clientId = sanitizeClientId(req.body?.clientId); + if (!clientId) return badRequest(res, 'clientId invalide (a-z, 0-9, -, 2 à 65 caractères)'); + const clientName = sanitizeClientName(req.body?.clientName); + if (!clientName) return badRequest(res, 'clientName requis (≥2 caractères)'); + const redirectUris = sanitizeRedirectUris(req.body?.redirectUris); + if (!redirectUris) return badRequest(res, 'redirectUris requis (≥1 URI HTTPS ou loopback http)'); + const allowedScopes = sanitizeScopes(req.body?.allowedScopes); + if (!allowedScopes) return badRequest(res, 'allowedScopes requis (≥1 scope connu)'); + const description = sanitizeDescription(req.body?.description); + const homepageUrl = sanitizeHttpUrl(req.body?.homepageUrl); + const publicClient = req.body?.publicClient !== false; + const requirePkce = publicClient ? true : req.body?.requirePkce === true; + const generatedSecret = !publicClient ? generateClientSecret() : null; + + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (existing) return badRequest(res, 'Cet clientId existe déjà'); + + const now = Date.now(); + await pool.execute( + `INSERT INTO oauth_clients + (client_id, client_name, description, homepage_url, redirect_uris, + allowed_scopes, public_client, require_pkce, client_secret, + is_active, vip_days_balance, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)`, + [ + clientId, + clientName, + description, + homepageUrl, + JSON.stringify(redirectUris), + JSON.stringify(allowedScopes), + publicClient ? 1 : 0, + requirePkce ? 1 : 0, + generatedSecret, + now, + now, + ], + ); + + await oauthClientsDb.reloadCache(); + const row = await fetchAppByClientId(pool, clientId); + const serialized = serializeAppRow(row); + // Le secret n'est exposé qu'UNE fois (à la création) — l'admin doit le copier. + return res.json({ + success: true, + app: serialized, + clientSecret: generatedSecret, // null pour les clients publics + }); + } catch (err) { + return serverError(res, err, 'Impossible de créer l\'application'); + } +}); + +router.put('/:clientId', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (!existing) return notFound(res); + + const updates = []; + const params = []; + + if (req.body?.clientName !== undefined) { + const v = sanitizeClientName(req.body.clientName); + if (!v) return badRequest(res, 'clientName invalide'); + updates.push('client_name = ?'); params.push(v); + } + if (req.body?.description !== undefined) { + updates.push('description = ?'); params.push(sanitizeDescription(req.body.description)); + } + if (req.body?.homepageUrl !== undefined) { + updates.push('homepage_url = ?'); params.push(sanitizeHttpUrl(req.body.homepageUrl)); + } + if (req.body?.redirectUris !== undefined) { + const v = sanitizeRedirectUris(req.body.redirectUris); + if (!v) return badRequest(res, 'redirectUris invalide (≥1 URI HTTPS ou loopback http)'); + updates.push('redirect_uris = ?'); params.push(JSON.stringify(v)); + } + if (req.body?.allowedScopes !== undefined) { + const v = sanitizeScopes(req.body.allowedScopes); + if (!v) return badRequest(res, 'allowedScopes invalide'); + updates.push('allowed_scopes = ?'); params.push(JSON.stringify(v)); + } + if (req.body?.publicClient !== undefined) { + const becomesPublic = req.body.publicClient === true; + updates.push('public_client = ?'); params.push(becomesPublic ? 1 : 0); + if (becomesPublic) { + // Switch confidential → public : on force pkce et on supprime le secret. + updates.push('require_pkce = 1'); + updates.push('client_secret = NULL'); + } + } + if (req.body?.requirePkce !== undefined) { + updates.push('require_pkce = ?'); params.push(req.body.requirePkce === true ? 1 : 0); + } + if (req.body?.isActive !== undefined) { + updates.push('is_active = ?'); params.push(req.body.isActive === true ? 1 : 0); + } + + if (updates.length === 0) return badRequest(res, 'Aucun champ à mettre à jour'); + + updates.push('updated_at = ?'); params.push(Date.now()); + params.push(clientId); + + await pool.execute( + `UPDATE oauth_clients SET ${updates.join(', ')} WHERE client_id = ?`, + params, + ); + + await oauthClientsDb.reloadCache(); + const row = await fetchAppByClientId(pool, clientId); + return res.json({ success: true, app: serializeAppRow(row) }); + } catch (err) { + return serverError(res, err, 'Impossible de mettre à jour l\'application'); + } +}); + +router.post('/:clientId/regenerate-secret', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (!existing) return notFound(res); + if (existing.public_client === 1 || existing.public_client === true) { + return badRequest(res, 'Les clients publics n\'utilisent pas de clientSecret'); + } + const newSecret = generateClientSecret(); + await pool.execute( + 'UPDATE oauth_clients SET client_secret = ?, updated_at = ? WHERE client_id = ?', + [newSecret, Date.now(), clientId], + ); + await oauthClientsDb.reloadCache(); + return res.json({ success: true, clientSecret: newSecret }); + } catch (err) { + return serverError(res, err, 'Impossible de régénérer le secret'); + } +}); + +router.delete('/:clientId', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (!existing) return notFound(res); + // Hard delete : on supprime la ligne ; les stats et grants restent + // (FK absente volontairement — historique d'audit). + await pool.execute('DELETE FROM oauth_clients WHERE client_id = ?', [clientId]); + // Cleanup icône si présente. + if (existing.icon_filename) { + await removeIconFile(existing.icon_filename); + } + await oauthClientsDb.reloadCache(); + return res.json({ success: true }); + } catch (err) { + return serverError(res, err, 'Impossible de supprimer l\'application'); + } +}); + +// Upload icône : JSON body { mimeType, dataBase64 } +// On évite multer pour ne pas ajouter une dépendance ; les icônes sont +// petites (< 256KB) donc base64 dans le body JSON est OK. +router.post('/:clientId/icon', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const mimeType = String(req.body?.mimeType || '').trim().toLowerCase(); + const ext = ALLOWED_ICON_MIME[mimeType]; + if (!ext) return badRequest(res, 'mimeType non supporté (png / jpeg / webp)'); + const dataBase64 = String(req.body?.dataBase64 || ''); + if (!dataBase64) return badRequest(res, 'dataBase64 requis'); + + let buffer; + try { + buffer = Buffer.from(dataBase64, 'base64'); + } catch { + return badRequest(res, 'dataBase64 invalide'); + } + if (buffer.length === 0) return badRequest(res, 'Fichier vide'); + if (buffer.length > MAX_ICON_SIZE_BYTES) { + return badRequest(res, `Fichier trop gros (max ${Math.round(MAX_ICON_SIZE_BYTES / 1024)} KB)`); + } + + // Vérification rapide du magic number pour bloquer un PNG renommé en .jpg etc. + const isPng = buffer.length >= 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47; + const isJpeg = buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff; + const isWebp = buffer.length >= 12 + && buffer.slice(0, 4).toString('ascii') === 'RIFF' + && buffer.slice(8, 12).toString('ascii') === 'WEBP'; + if ((ext === 'png' && !isPng) || (ext === 'jpg' && !isJpeg) || (ext === 'webp' && !isWebp)) { + return badRequest(res, 'Le contenu ne correspond pas au mimeType déclaré'); + } + + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (!existing) return notFound(res); + + // Ensure dir exists (sécurité : ICON_DIR géré par ensureTables au boot). + if (!fs.existsSync(oauthClientsDb.ICON_DIR)) { + fs.mkdirSync(oauthClientsDb.ICON_DIR, { recursive: true, mode: 0o755 }); + } + + const filename = `${clientId}-${Date.now()}.${ext}`; + const targetPath = path.join(oauthClientsDb.ICON_DIR, filename); + await fsp.writeFile(targetPath, buffer, { mode: 0o644 }); + + // Cleanup ancienne icône avant d'enregistrer la nouvelle. + const previousFilename = existing.icon_filename; + await pool.execute( + 'UPDATE oauth_clients SET icon_filename = ?, updated_at = ? WHERE client_id = ?', + [filename, Date.now(), clientId], + ); + if (previousFilename && previousFilename !== filename) { + await removeIconFile(previousFilename); + } + + await oauthClientsDb.reloadCache(); + return res.json({ + success: true, + iconFilename: filename, + iconUrl: `/oauth-icons/${filename}`, + }); + } catch (err) { + return serverError(res, err, 'Impossible d\'uploader l\'icône'); + } +}); + +router.delete('/:clientId/icon', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const pool = getPool(); + const existing = await fetchAppByClientId(pool, clientId); + if (!existing) return notFound(res); + if (existing.icon_filename) { + await removeIconFile(existing.icon_filename); + await pool.execute( + 'UPDATE oauth_clients SET icon_filename = NULL, updated_at = ? WHERE client_id = ?', + [Date.now(), clientId], + ); + await oauthClientsDb.reloadCache(); + } + return res.json({ success: true }); + } catch (err) { + return serverError(res, err, 'Impossible de supprimer l\'icône'); + } +}); + +// Alimente le compteur de jours VIP que l'app peut distribuer. +// Body : { delta: number } → positif (ajoute) ou négatif (retire, sans descendre sous 0). +router.post('/:clientId/vip-balance', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const delta = Number(req.body?.delta); + if (!Number.isInteger(delta) || delta === 0) { + return badRequest(res, 'delta doit être un entier non nul'); + } + if (Math.abs(delta) > 100000) { + return badRequest(res, 'delta trop grand'); + } + + const pool = getPool(); + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + const [rows] = await conn.execute( + 'SELECT id, vip_days_balance FROM oauth_clients WHERE client_id = ? FOR UPDATE', + [clientId], + ); + if (rows.length === 0) { + await conn.rollback(); + return notFound(res); + } + const current = Number(rows[0].vip_days_balance || 0); + const next = Math.max(0, current + delta); // clamp à 0 pour éviter un balance négatif + await conn.execute( + 'UPDATE oauth_clients SET vip_days_balance = ?, updated_at = ? WHERE id = ?', + [next, Date.now(), rows[0].id], + ); + await conn.commit(); + await oauthClientsDb.reloadCache(); + return res.json({ + success: true, + previousBalance: current, + newBalance: next, + deltaApplied: next - current, + }); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } + } catch (err) { + return serverError(res, err, 'Impossible de mettre à jour le balance VIP'); + } +}); + +router.get('/:clientId/stats', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const sinceDays = Math.min(Math.max(Number(req.query?.sinceDays) || 30, 1), 365); + const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000; + const stats = await oauthClientsDb.getStats(clientId, sinceMs); + if (!stats) return serverError(res, null, 'DB indisponible'); + return res.json({ success: true, sinceDays, ...stats }); + } catch (err) { + return serverError(res, err, 'Impossible de récupérer les stats'); + } +}); + +router.get('/:clientId/grants', async (req, res) => { + try { + const clientId = sanitizeClientId(req.params.clientId); + if (!clientId) return badRequest(res, 'clientId invalide'); + const limit = Math.min(Math.max(Number(req.query?.limit) || 50, 1), 500); + const pool = getPool(); + const [rows] = await pool.execute( + `SELECT id, client_id, user_id, user_type, user_id_only, days_granted, + access_key_value, expires_at, granted_at, revoked_at + FROM oauth_vip_grants + WHERE client_id = ? + ORDER BY granted_at DESC + LIMIT ?`, + [clientId, limit], + ); + const grants = rows.map((row) => ({ + id: Number(row.id), + clientId: row.client_id, + userId: row.user_id, + userType: row.user_type, + userIdOnly: row.user_id_only, + daysGranted: Number(row.days_granted), + // accessKey n'est PAS retournée — c'est un secret porté à l'user. + // On expose juste les 4 derniers chars pour identifier. + accessKeyHint: typeof row.access_key_value === 'string' && row.access_key_value.length > 4 + ? `…${row.access_key_value.slice(-4)}` + : null, + expiresAt: row.expires_at, + grantedAt: Number(row.granted_at), + revokedAt: row.revoked_at ? Number(row.revoked_at) : null, + })); + return res.json({ success: true, grants }); + } catch (err) { + return serverError(res, err, 'Impossible de récupérer les grants'); + } +}); + +module.exports = router; diff --git a/API/Mainapi/routes/coflix.js b/API/Mainapi/routes/coflix.js index f50894d..0347688 100644 --- a/API/Mainapi/routes/coflix.js +++ b/API/Mainapi/routes/coflix.js @@ -680,7 +680,7 @@ async function getTvDataFromCoflix(url, seasonNumber, episodeNumber) { (ep) => parseInt(ep.number) === parseInt(episodeNumber), ); if (episode && episode.links) { - episodeUrl = episode.links.startsWith("https://coflix.click") + episodeUrl = episode.links.startsWith("https://coflix.date") ? `${episode.links}` : episode.links; } @@ -688,7 +688,7 @@ async function getTvDataFromCoflix(url, seasonNumber, episodeNumber) { } if (!episodeUrl) { - episodeUrl = `https://coflix.click/episode/${seriesSlug}-${seasonNumber}x${episodeNumber}/`; + episodeUrl = `https://coflix.date/episode/${seriesSlug}-${seasonNumber}x${episodeNumber}/`; } try { diff --git a/API/Mainapi/routes/darkiworld.js b/API/Mainapi/routes/darkiworld.js index bbd2404..8b4063a 100644 --- a/API/Mainapi/routes/darkiworld.js +++ b/API/Mainapi/routes/darkiworld.js @@ -13,6 +13,13 @@ const fsp = require('fs').promises; const { generateCacheKey } = require('../utils/cacheManager'); const { getAuthIfValid } = require('../middleware/auth'); const { getPool: getMovixPool } = require('../mysqlPool'); +const hydrackerQueue = require('../utils/hydrackerQueue'); + +// TTL pour les échecs de /decode (ex. "Lien d'embed invalide" persistant côté +// upstream). On stocke un marker `{ failed: true, failedAt }` au lieu du +// cachedData habituel, pour servir directement un 404 pendant ce délai sans +// re-taper hydracker.com à chaque clic. +const DECODE_FAILED_TTL_MS = 2 * 60 * 60 * 1000; const HOST_ICON_MAP = { '1fichier': '/hosts/1fichier.svg', @@ -23,6 +30,9 @@ const HOST_ICON_MAP = { 'Dropbox': '/hosts/dropbox.svg', }; +// Hydracker uploaders to filter out (unreliable / spam). Hardcoded; not env-driven. +const BLOCKED_DARKIWORLD_USERS = new Set(['Guest']); + async function resolveMovixUsername(userId, authType) { try { const safeUserId = String(userId).replace(/[^a-zA-Z0-9_\-]/g, ''); @@ -102,6 +112,8 @@ let saveToCache; let shouldUpdateCache; let shouldUpdateCache24h; let refreshDarkinoSessionIfNeeded; +let redis; +let shouldUpdateCache48h; /** * Inject runtime dependencies that still live in server.js. @@ -116,6 +128,8 @@ function configure(deps) { if (deps.shouldUpdateCache) shouldUpdateCache = deps.shouldUpdateCache; if (deps.shouldUpdateCache24h) shouldUpdateCache24h = deps.shouldUpdateCache24h; if (deps.refreshDarkinoSessionIfNeeded) refreshDarkinoSessionIfNeeded = deps.refreshDarkinoSessionIfNeeded; + if (deps.redis) redis = deps.redis; + if (deps.shouldUpdateCache48h) shouldUpdateCache48h = deps.shouldUpdateCache48h; } function parsePositiveInt(value, fallback) { @@ -296,6 +310,34 @@ async function findAllEntriesForEpisode({ titleId, seasonId, episodeId, perPage return foundEntries; } +// --------------------------------------------------------------------------- +// partitionLinksByDecodeCache — orders darkiworld links so that entries with +// a successful decode cache file on disk appear first. Cache file presence is +// verified via getFromCacheNoExpiration; only `success === true` payloads +// count (failed markers and missing files do NOT count as "cached"). +// Movix links are passed through untouched (caller prepends them). +// --------------------------------------------------------------------------- +async function partitionLinksByDecodeCache(links) { + if (!Array.isArray(links) || links.length === 0) return links || []; + const probes = await Promise.all(links.map(async (link) => { + if (link?.id == null) return { link, available: false }; + try { + const cacheKey = generateCacheKey(`darkiworld_decode_v2_${link.id}`); + const payload = await getFromCacheNoExpiration(DOWNLOAD_CACHE_DIR, cacheKey); + return { link, available: payload?.success === true }; + } catch (_) { + return { link, available: false }; + } + })); + const available = []; + const rest = []; + for (const { link, available: ok } of probes) { + if (ok) available.push(link); + else rest.push(link); + } + return [...available, ...rest]; +} + // --------------------------------------------------------------------------- // GET /download/:type/:id // Récupérer tous les liens d'amélioration DarkiWorld pour un film ou un épisode @@ -339,16 +381,46 @@ router.get('/download/:type/:id', async (req, res) => { let dataReturned = false; if (cachedData) { - // console.log(`Résultats de téléchargement pour ${type}/${id} récupérés du cache`); + // Re-sort by disk decode cache presence on every read so previously + // decoded entries (from past clicks / past prewarms) bubble to the top + // even though the list cache itself is frozen between writes. const cachedAll = Array.isArray(cachedData?.all) ? cachedData.all.map(r => ({ ...r, source: r.source || 'darkiworld' })) : []; - res.status(200).json({ ...cachedData, all: [...movixLinks, ...cachedAll], movixCount: movixLinks.length }); + const sortedCachedAll = await partitionLinksByDecodeCache(cachedAll); + res.status(200).json({ ...cachedData, all: [...movixLinks, ...sortedCachedAll], movixCount: movixLinks.length }); dataReturned = true; } + // Determine refresh staleness once and reuse below. shouldUpdateCache + // returns true if the file is missing OR older than 40 min. + const cacheNeedsUpdate = await shouldUpdateCache(DOWNLOAD_CACHE_DIR, cacheKey); + // Vérifier si l'utilisateur a accès premium (optionnel) const auth = await getAuthIfValid(req); const darkiworld_premium = auth && auth.userType === 'premium'; + // Pre-warm decode cache via hydracker's new /download endpoint. Fires on + // cache miss AND on stale cache (>40min) — the new /download endpoint is + // separate from the /decode endpoint that gets rate-limited, so prewarm + // can keep the decode cache hot for the curated subset even during a + // rate-limit window. Best-effort: a failure here is a no-op for the + // response — the queue still handles uncached ids on click. + const prewarmPromise = cacheNeedsUpdate + ? hydrackerQueue.prewarmDecodeCache({ + type, id, season, episode, + deps: { + axiosDarkinoRequest, + refreshDarkinoSessionIfNeeded, + cacheDir: DOWNLOAD_CACHE_DIR, + generateCacheKey, + saveToCache, + blockedUsers: BLOCKED_DARKIWORLD_USERS + } + }).catch((e) => { + console.warn(`[hydracker] prewarm launcher caught: ${e?.message || e}`); + return { warmed: 0, warmedIds: new Set() }; + }) + : Promise.resolve({ warmed: 0, warmedIds: new Set() }); + let allEnhancementLinks = []; if (type === 'movie') { @@ -364,7 +436,8 @@ router.get('/download/:type/:id', async (req, res) => { url: `/api/v1/titles/${id}/content/liens?perPage=100&loader=linksdl&filters=&paginate=preferLengthAware` }); - const allEntries = liensResp.data?.pagination?.data || []; + const rawEntries = liensResp.data?.pagination?.data || []; + const allEntries = rawEntries.filter(e => !BLOCKED_DARKIWORLD_USERS.has(e?.id_user)); // Traiter directement les entrées sans faire de requête de décodage const enhancementSources = allEntries.map(entry => { @@ -372,16 +445,9 @@ router.get('/download/:type/:id', async (req, res) => { const hostInfo = entry.host; const provider = hostInfo?.name || 'unknown'; - // Pour darkibox, le `lien` direct n'est pas exploitable, on garde - // null afin que le frontend déclenche /decode pour construire - // l'URL d'embed. Pour les autres providers (1fichier, sendcm, …), - // `entry.lien` est l'URL de téléchargement directe. - const directLien = provider.toLowerCase() === 'darkibox' ? null : (entry.lien || null); return { id: entry.id, - lien: directLien, - id_user: entry.id_user || undefined, language: (entry?.langues_compact && entry.langues_compact.length > 0) ? entry.langues_compact.map(l => l.name).join(', ') : undefined, @@ -406,7 +472,7 @@ router.get('/download/:type/:id', async (req, res) => { // Pour les séries (épisodes) try { // 1. Paginer intelligemment pour trouver l'épisode - const allEntries = await findAllEntriesForEpisode({ + const rawEntries = await findAllEntriesForEpisode({ titleId: id, seasonId: parseInt(season), episodeId: parseInt(episode), @@ -414,21 +480,17 @@ router.get('/download/:type/:id', async (req, res) => { maxPages: 10 }); + const allEntries = rawEntries.filter(e => !BLOCKED_DARKIWORLD_USERS.has(e?.id_user)); + // Traiter directement les entrées sans faire de requête de décodage const enhancementSources = allEntries.map(entry => { if (!entry) return null; const hostInfo = entry.host; const provider = hostInfo?.name || 'unknown'; - // Pour darkibox, on laisse le frontend appeler /decode (URL d'embed - // construite côté serveur). Pour les autres providers, le `lien` - // est l'URL directe de téléchargement. - const directLien = provider.toLowerCase() === 'darkibox' ? null : (entry.lien || null); return { id: entry.id, - lien: directLien, - id_user: entry.id_user || undefined, language: (entry?.langues_compact && entry.langues_compact.length > 0) ? entry.langues_compact.map(l => l.name).join(', ') : undefined, @@ -455,7 +517,22 @@ router.get('/download/:type/:id', async (req, res) => { } catch (_) { /* upstream failure leaves allEnhancementLinks empty */ } } - const taggedDarkiLinks = allEnhancementLinks.map(r => ({ ...r, source: 'darkiworld' })); + // Wait for the prewarm to settle so the disk decode cache is hot before + // we respond — without this await the client could click a link before + // the pre-warmed entry was written and would needlessly hit the queue. + const prewarmResult = await prewarmPromise; + if (prewarmResult?.warmed) { + console.log(`[hydracker] prewarm ${type}/${id} warmed=${prewarmResult.warmed}`); + } + + // Sort by disk decode cache presence: anything with an existing + // success-shaped payload at darkiworld_decode_v2_{id} (whether from a + // prior queue decode, a prior decodeRequestSync, or this request's + // prewarm) bubbles to the top. The prewarm just completed above so its + // writes are already on disk and counted here. + const orderedEnhancementLinks = await partitionLinksByDecodeCache(allEnhancementLinks); + + const taggedDarkiLinks = orderedEnhancementLinks.map(r => ({ ...r, source: 'darkiworld' })); const responseData = { success: true, all: [...movixLinks, ...taggedDarkiLinks], @@ -467,21 +544,20 @@ router.get('/download/:type/:id', async (req, res) => { res.json(responseData); } - // Background update du cache + // Background update du cache — reuses cacheNeedsUpdate computed above + // so we don't restat the file twice per request. (async () => { try { - // Vérifier si le cache doit être mis à jour - const shouldUpdate = await shouldUpdateCache(DOWNLOAD_CACHE_DIR, cacheKey); - if (!shouldUpdate) { - return; // Ne pas mettre à jour le cache + if (!cacheNeedsUpdate) { + return; // Cache encore frais (<40 min), pas de réécriture. } // Si on a des données, sauvegarder dans le cache - if (allEnhancementLinks && allEnhancementLinks.length > 0) { + if (orderedEnhancementLinks && orderedEnhancementLinks.length > 0) { // Store only DarkiWorld entries in cache — Movix links are fetched fresh each request const darkiOnlyData = { success: true, - all: allEnhancementLinks.map(r => ({ ...r, source: 'darkiworld' })) + all: orderedEnhancementLinks.map(r => ({ ...r, source: 'darkiworld' })) }; await saveToCache(DOWNLOAD_CACHE_DIR, cacheKey, darkiOnlyData); } @@ -534,189 +610,62 @@ router.get('/download/:type/:id', async (req, res) => { router.get('/decode/:id', async (req, res) => { try { const { id } = req.params; - const { title_id: titleIdParam } = req.query; - const titleId = titleIdParam ? String(titleIdParam) : null; + if (!id) return res.status(400).json({ success: false, error: 'ID du lien requis' }); - if (!id) { - return res.status(400).json({ - success: false, - error: 'ID du lien requis' - }); - } - - // Cache key v2 — invalide les anciens caches qui ont stocké des - // URLs d'embed invalides retournées par /api/v1/liens/{id}/download. - const cacheKey = generateCacheKey(`darkiworld_decode_v2_${id}`); - - // Check if results are in cache without expiration (stale-while-revalidate) - const cachedData = await getFromCacheNoExpiration(DOWNLOAD_CACHE_DIR, cacheKey); - let dataReturned = false; - let shouldDoBackgroundUpdate = false; - - if (cachedData) { - // Vérifier si le lien en cache est un lien d'embed invalide - const embedUrl = cachedData.embed_url || ''; - const isInvalidEmbedLink = /\/embed-\d+\.html$/i.test(embedUrl); - - if (isInvalidEmbedLink) { - // Cache invalide (lien d'embed `/embed-NN.html`), refetch silencieusement - } else { - // Cache valide, le retourner immédiatement - res.status(200).json(cachedData); - dataReturned = true; - - // Vérifier si on doit faire un background update (fichier modifié il y a plus de 24h) - shouldDoBackgroundUpdate = await shouldUpdateCache24h(DOWNLOAD_CACHE_DIR, cacheKey); - - // Si pas besoin de background update, on s'arrête là - if (!shouldDoBackgroundUpdate) { - return; - } - // Sinon on continue pour faire le fetch en arrière-plan - } - } - - - - // Si pas de cache valide - // Si en maintenance if (DARKINO_MAINTENANCE) { - if (!dataReturned) { - return res.status(200).json({ error: 'Service Darkino temporairement indisponible (maintenance)' }); - } - return; // Si on a déjà retourné des données, on arrête juste le traitement (pas de background update) + return res.status(200).json({ error: 'Service Darkino temporairement indisponible (maintenance)' }); } - // Fonction pour récupérer les données fraîches via l'ancien endpoint - // `/api/v1/liens/{id}/download`. Le `title_id` est ignoré côté backend - // pour le moment (le nouvel endpoint content/liens requiert une session - // authentifiée), mais reste accepté en query pour compat future. - void titleId; - const fetchFreshData = async () => { - let linkInfo = null; - let embedUrl = null; - let provider = 'unknown'; - let retryCount = 0; - const maxRetries = 2; + const result = hydrackerQueue.BATCHING_ENABLED + ? await hydrackerQueue.decodeRequest(id, { + redis, + cacheDir: DOWNLOAD_CACHE_DIR, + generateCacheKey, + getFromCacheNoExpiration + }) + : await hydrackerQueue.decodeRequestSync(id, { + cacheDir: DOWNLOAD_CACHE_DIR, + generateCacheKey, + getFromCacheNoExpiration, + saveToCache, + axiosDarkinoRequest, + refreshDarkinoSessionIfNeeded + }); - while (retryCount < maxRetries) { - try { - await refreshDarkinoSessionIfNeeded(); - - const linkResp = await axiosDarkinoRequest({ - method: 'get', - url: `/api/v1/liens/${id}/download` - }); - - linkInfo = linkResp.data; - provider = linkInfo?.host?.name || 'unknown'; - - if (provider === 'darkibox') { - const rawDarkiboxLink = typeof linkInfo?.lien === 'string' ? linkInfo.lien : ''; - const darkiboxCodeMatch = rawDarkiboxLink.match(/darkibox\.com\/(?:embed-)?([a-z0-9]{12,})(?:\.html)?/i); - const darkiboxCode = darkiboxCodeMatch ? darkiboxCodeMatch[1] : null; - embedUrl = darkiboxCode - ? `https://darkibox.com/embed-${darkiboxCode}.html` - : (rawDarkiboxLink || `https://darkibox.com/embed-${id}.html`); - } else { - embedUrl = linkInfo?.lien || `https://darkibox.com/embed-${id}.html`; - } - - const isInvalidEmbedLink = /\/embed-\d+\.html$/i.test(embedUrl); - - if (isInvalidEmbedLink && retryCount < maxRetries - 1) { - retryCount++; - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } else if (isInvalidEmbedLink) { - throw new Error('Lien d\'embed invalide'); - } - - break; - } catch (err) { - if (retryCount < maxRetries - 1) { - retryCount++; - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - throw err; - } - } - - return { - success: true, - id: id, - provider: provider, - embed_url: embedUrl, - metadata: linkInfo ? { - language: (linkInfo?.langues_compact && linkInfo.langues_compact.length > 0) - ? linkInfo.langues_compact.map(l => l.name).join(', ') - : undefined, - quality: linkInfo?.qual?.qual, - sub: (linkInfo?.subs_compact && linkInfo.subs_compact.length > 0) - ? linkInfo.subs_compact.map(s => s.name).join(', ') - : undefined, - size: linkInfo?.size, - upload_date: linkInfo?.created_at - } : null - }; - }; - - // Si on fait un background update (données déjà retournées au client) - if (dataReturned && shouldDoBackgroundUpdate) { - // Background update du cache - (async () => { - try { - const freshData = await fetchFreshData(); - if (freshData && freshData.embed_url) { - await saveToCache(DOWNLOAD_CACHE_DIR, cacheKey, freshData); - } - } catch (bgError) { - // Silent fail on background update - } - })(); - return; - } - - // Si on n'a pas encore retourné de données, faire le fetch normal - try { - const responseData = await fetchFreshData(); - - // Retourner les données - res.json(responseData); - - // Mise à jour du cache - if (responseData.embed_url) { - try { - await saveToCache(DOWNLOAD_CACHE_DIR, cacheKey, responseData); - } catch (cacheError) { - // Silent fail on cache save - } - } - } catch (fetchError) { + if (result.payload) return res.status(200).json(result.payload); + if (result.failed) { return res.status(404).json({ success: false, - error: 'Lien non trouvé ou inaccessible', - id: id, - debug: fetchError.message + error: result.failed.error || 'Lien non trouvé ou inaccessible', + id: result.failed.id || id, + debug: result.failed.debug || '' + }); + } + if (result.queued) { + return res.status(202).json({ + status: 'queued', + queue_size: result.queue_size, + id + }); + } + if (result.rateLimited) { + return res.status(503).json({ + success: false, + error: 'rate_limited', + retry_at: result.retryAt + }); + } + if (result.unavailable) { + return res.status(503).json({ + success: false, + error: 'queue_unavailable', + message: 'Infrastructure indisponible, réessaie plus tard' }); } + return res.status(500).json({ success: false, error: 'Unknown decode result' }); + } catch (error) { - if (res.headersSent) { - return; - } - - // Si Darkino retourne 500, ne pas créer/mettre à jour le cache et renvoyer le cache existant si présent - if (error.response && error.response.status >= 500) { - try { - const fallbackCache = await getFromCacheNoExpiration(DOWNLOAD_CACHE_DIR, cacheKey); - if (fallbackCache) { - return res.status(200).json(fallbackCache); - } - } catch (_) { } - } - if (!res.headersSent) { res.status(500).json({ success: false, diff --git a/API/Mainapi/routes/downloadLinksLeaderboard.js b/API/Mainapi/routes/downloadLinksLeaderboard.js index 3d2318c..5856021 100644 --- a/API/Mainapi/routes/downloadLinksLeaderboard.js +++ b/API/Mainapi/routes/downloadLinksLeaderboard.js @@ -1,23 +1,8 @@ const express = require('express'); const router = express.Router(); -const path = require('path'); -const fsp = require('fs').promises; const { getPool } = require('../mysqlPool'); const { isUploaderOrAdmin } = require('../middleware/auth'); - -async function getUserData(userId, userType) { - try { - const safeUserId = String(userId).replace(/[^a-zA-Z0-9_\-]/g, ''); - const safeUserType = userType === 'bip39' ? 'bip39' : 'oauth'; - const userPath = path.join(__dirname, '..', 'data', 'users', safeUserType, `${safeUserId}.json`); - const data = JSON.parse(await fsp.readFile(userPath, 'utf8')); - if (data.profiles && data.profiles.length > 0) { - const p = data.profiles[0]; - return { username: p.name || 'Admin', avatar: p.avatar || null }; - } - } catch { /* fall through */ } - return { username: 'Admin', avatar: null }; -} +const { resolveAdminIdentity } = require('../utils/adminIdentity'); router.get('/admin/leaderboard', isUploaderOrAdmin, async (req, res) => { try { @@ -75,14 +60,13 @@ router.get('/admin/leaderboard', isUploaderOrAdmin, async (req, res) => { } const leaderboard = await Promise.all(rows.map(async (row) => { - const userType = row.admin_auth_type === 'bip-39' ? 'bip39' : 'oauth'; - const u = await getUserData(row.admin_id, userType); + const identity = await resolveAdminIdentity(row.admin_id, row.admin_auth_type); return { admin_id: row.admin_id, admin_auth_type: row.admin_auth_type, role: adminRoles[row.admin_id] || 'admin', - username: u.username, - avatar: u.avatar, + username: identity.username, + avatar: identity.avatar, score: Number(row.score), last_action_at: row.last_action_at, }; diff --git a/API/Mainapi/routes/francetv.js b/API/Mainapi/routes/francetv.js index 27a2ad7..4ea8750 100644 --- a/API/Mainapi/routes/francetv.js +++ b/API/Mainapi/routes/francetv.js @@ -58,16 +58,18 @@ function configure(deps) { // getFtvNextActionHash -- dynamically retrieve the Next.js server action hash // --------------------------------------------------------------------------- /** - * Récupère dynamiquement le hash next-action depuis la page /recherche/ + * Récupère dynamiquement le hash next-action depuis la page /recherche/. * Ce hash change à chaque redéploiement de france.tv (Next.js Server Actions). * * Étapes : * 1. GET https://www.france.tv/recherche/ - * 2. Trouver le + diff --git a/package-lock.json b/package-lock.json index e20c6bc..54739bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,55 +14,48 @@ "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", - "@firebase/firestore": "^4.9.2", "@headlessui/react": "^2.2.0", "@hono/node-server": "^1.19.14", - "@noble/hashes": "^1.8.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", - "@react-hook/resize-observer": "^2.0.2", + "@tanstack/react-virtual": "^3.13.24", "axios": "^1.13.2", "canvas-confetti": "^1.9.4", - "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dashjs": "^5.1.0", + "dashjs": "^5.1.1", "date-fns": "^4.1.0", "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "emoji-mart": "^5.6.0", - "emoji-picker-react": "^4.12.2", "file-saver": "^2.0.5", - "firebase": "^12.6.0", "framer-motion": "11.11.10", "hls.js": "^1.6.16", "hono": "^4.12.14", "i18next": "^25.8.10", "i18next-browser-languagedetector": "^8.2.1", "jszip": "^3.10.1", - "lenis": "^1.3.17", + "lenis": "^1.3.23", "lucide-react": "^0.344.0", "mpegts.js": "^1.8.0", "pako": "^2.1.0", "qrcode": "^1.5.4", "react": "^18.3.1", - "react-chartjs-2": "^5.3.0", "react-colorful": "^5.6.1", "react-country-flag": "^3.1.0", "react-dom": "^18.3.1", "react-force-graph-2d": "^1.29.1", "react-helmet-async": "^2.0.5", "react-i18next": "^16.5.4", - "react-icons": "^5.5.0", "react-input-mask": "^2.0.4", + "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-router-dom": "^6.22.3", - "react-snowfall": "^2.3.0", - "recharts": "^2.15.3", + "react-snowfall": "^2.4.0", "remark-emoji": "^5.0.1", "remark-gfm": "^4.0.1", "shaka-player": "^5.0.0", @@ -72,12 +65,7 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "video.js": "^8.23.4", - "web-haptics": "^0.0.6", - "workbox-core": "^7.3.0", - "workbox-precaching": "^7.3.0", - "workbox-routing": "^7.3.0", - "workbox-strategies": "^7.3.0", - "workbox-window": "^7.3.0" + "web-haptics": "^0.0.6" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -87,8 +75,8 @@ "@types/react-input-mask": "^3.0.6", "@types/uuid": "^10.0.0", "@types/video.js": "^7.3.58", - "@vitejs/plugin-basic-ssl": "^1.2.0", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-basic-ssl": "^2.3.0", + "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", @@ -96,10 +84,11 @@ "eslint-plugin-unused-imports": "^4.3.0", "globals": "^15.9.0", "postcss": "^8.4.35", + "rollup-plugin-visualizer": "^7.0.1", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2", + "vite": "^8.0.11", "wrangler": "^4.15.1" } }, @@ -115,240 +104,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -358,54 +113,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", @@ -608,6 +315,18 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -619,6 +338,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emoji-mart/data": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", @@ -635,295 +365,6 @@ "react": "^16.8 || ^17 || ^18" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", @@ -941,23 +382,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", @@ -975,23 +399,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", @@ -1009,74 +416,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1234,632 +573,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@firebase/ai": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.0.tgz", - "integrity": "sha512-NGyE7NQDFznOv683Xk4+WoUv39iipa9lEfrwvvPz33ChzVbCCiB69FJQTK2BI/11pRtzYGbHo1/xMz7gxWWhJw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/analytics": { - "version": "0.10.19", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", - "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/analytics-compat": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", - "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/analytics": "0.10.19", - "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/analytics-types": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", - "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", - "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/app-check": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", - "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/app-check-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", - "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check": "0.11.0", - "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", - "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-check-types": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", - "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-compat": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", - "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app": "0.14.6", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/app-types": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", - "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app/node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, - "node_modules/@firebase/auth-compat": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", - "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth": "1.11.1", - "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", - "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@react-native-async-storage/async-storage": "^1.18.1" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", - "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/auth-types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", - "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/component": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", - "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/data-connect": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz", - "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/database": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", - "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", - "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/database": "1.1.0", - "@firebase/database-types": "1.0.16", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-types": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", - "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.3", - "@firebase/util": "1.13.0" - } - }, - "node_modules/@firebase/firestore": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", - "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "@firebase/webchannel-wrapper": "1.0.5", - "@grpc/grpc-js": "~1.9.0", - "@grpc/proto-loader": "^0.7.8", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/firestore-compat": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", - "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/firestore": "4.9.2", - "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/firestore-types": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", - "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/functions": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", - "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/functions-compat": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", - "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/functions": "0.13.1", - "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/functions-types": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", - "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/installations": { - "version": "0.6.19", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", - "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/installations-compat": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", - "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/installations-types": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", - "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x" - } - }, - "node_modules/@firebase/installations/node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, - "node_modules/@firebase/logger": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", - "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/messaging": { - "version": "0.12.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", - "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.13.0", - "idb": "7.1.1", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/messaging-compat": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", - "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/messaging": "0.12.23", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/messaging-interop-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", - "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/messaging/node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "license": "ISC" - }, - "node_modules/@firebase/performance": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", - "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0", - "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/performance-compat": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", - "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/performance": "0.7.9", - "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/performance-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", - "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/remote-config": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", - "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/installations": "0.6.19", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/remote-config-compat": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", - "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/remote-config": "0.7.0", - "@firebase/remote-config-types": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/remote-config-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", - "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/storage": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", - "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x" - } - }, - "node_modules/@firebase/storage-compat": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", - "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/storage": "0.14.0", - "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - } - }, - "node_modules/@firebase/storage-types": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", - "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "1.x" - } - }, - "node_modules/@firebase/util": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", - "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/webchannel-wrapper": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", - "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", - "license": "Apache-2.0" - }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -1913,37 +626,6 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, - "node_modules/@grpc/grpc-js": { - "version": "1.9.15", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", - "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@headlessui/react": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", @@ -2576,17 +1258,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2612,22 +1283,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -2665,6 +1337,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", @@ -2720,70 +1402,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3649,37 +2267,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-hook/latest": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", - "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/passive-layout-effect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", - "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/resize-observer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-2.0.2.tgz", - "integrity": "sha512-tzKKzxNpfE5TWmxuv+5Ae3IF58n0FQgQaWJmcbYkjXTRZATXxClnTprQ2uuYygYTpu1pqbBskpwMpj6jpT1djA==", - "license": "MIT", - "dependencies": { - "@react-hook/latest": "^1.0.2", - "@react-hook/passive-layout-effect": "^1.2.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, "node_modules/@react-stately/flags": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", @@ -3719,31 +2306,10 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", "cpu": [ "arm64" ], @@ -3752,12 +2318,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", "cpu": [ "arm64" ], @@ -3766,12 +2335,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", "cpu": [ "x64" ], @@ -3780,26 +2352,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", "cpu": [ "x64" ], @@ -3808,46 +2369,32 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", "cpu": [ "arm64" ], @@ -3859,12 +2406,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", "cpu": [ "arm64" ], @@ -3876,46 +2426,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", "cpu": [ "ppc64" ], @@ -3927,63 +2446,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", "cpu": [ "s390x" ], @@ -3995,12 +2466,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", "cpu": [ "x64" ], @@ -4012,12 +2486,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", "cpu": [ "x64" ], @@ -4029,26 +2506,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", "cpu": [ "arm64" ], @@ -4057,12 +2523,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", "cpu": [ "arm64" ], @@ -4071,26 +2559,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", "cpu": [ "x64" ], @@ -4099,21 +2576,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", @@ -4140,15 +2613,129 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/@svta/common-media-library": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.17.4.tgz", - "integrity": "sha512-nP/KThzQW5FZKdc9V7ICTa9/A7xGw66VQoLPYOEwwMZTTrISp1zIQAX4KAYJw2PN/VPnxJQJXIYbzZTXgMHctw==", + "node_modules/@svta/cml-608": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.1.tgz", + "integrity": "sha512-Y/Ier9VPUSOBnf0bJqdDyTlPrt4dDB+jk5mYHa1bnD2kcRl8qn7KkW3PRuj4w1aVN+BS2eHmsLxodt7P2hylUg==", "license": "Apache-2.0", "engines": { "node": ">=20" } }, + "node_modules/@svta/cml-cmcd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-1.0.1.tgz", + "integrity": "sha512-eox305g+QUJgXqOLVrbgxeQHCgl90ewwQ9O2bIoo7m+hanR8Xswu5CknFnT5qqIbLOHfw80ug+raycoAFHTQ+w==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-cta": "1.0.1", + "@svta/cml-structured-field-values": "1.0.1", + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-cmsd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.1.tgz", + "integrity": "sha512-+nIB8PuSfb/qw+xGaArPhNqPm84tBJUbe3H1DnPL5QUsjSUI7mUIUQwAtRV1ZdEu0+80g9i0op79woB0OIwr/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-cta": "1.0.1", + "@svta/cml-structured-field-values": "1.0.1", + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-cta": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.1.tgz", + "integrity": "sha512-jcXqNIPv26bmFxIOFh8/c3+6WLH4qBjKpq9qTQcggDPoHuV1YBydMsJLOnYPDeK8rNMKcAkFLbnDRvyJthu5yw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-structured-field-values": "1.0.1", + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-dash": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.1.tgz", + "integrity": "sha512-lYnD1I7FUbbQND+xICI+kcRaRXuT+whKk27R8m8me5VMVu2sMsAMc7Yui6l9sxw2cBKt8pSETPYRm/1+n4LZkw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-id3": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.1.tgz", + "integrity": "sha512-90fGlL1qRI88CcaB89k6NG6cC3kky4Eu2jwqU4HefqK+S5k2OASUxf8JXkGz+DsdaiY7sh51vGPYdolfBZS7ug==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-request": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.1.tgz", + "integrity": "sha512-enL19BuXUjFkDDDF9jdNwUclMNPRsagnjGAetVC7xcmpDMpEx+ZLgsDip6BFNg5p6izSEk/OyujTWW1r8bDNiA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-utils": "1.0.1", + "@svta/cml-xml": "1.0.1" + } + }, + "node_modules/@svta/cml-structured-field-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.0.1.tgz", + "integrity": "sha512-Kibciki59Pon3Pn/sl5uyrbJcSpZQDKqdCfDrokBvOdLoqqcd0oFrkEPsZBiuuIODX1CB80612xe8hopeFDyBA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-utils": "1.0.1" + } + }, + "node_modules/@svta/cml-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.0.1.tgz", + "integrity": "sha512-kso3curTJfp00I1mKFoBliBApjn4aPE+wF8cPucf7TrSDVWZDeLLuF14ASmUE9m7rnrqTTK4878VvmXaXcCCfQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/@svta/cml-xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz", + "integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@svta/cml-utils": "1.0.1" + } + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -4159,12 +2746,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.13", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.13.tgz", - "integrity": "sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==", + "version": "3.13.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz", + "integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.13" + "@tanstack/virtual-core": "3.14.0" }, "funding": { "type": "github", @@ -4176,9 +2763,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.13", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz", - "integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", + "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==", "license": "MIT", "funding": { "type": "github", @@ -4191,114 +2778,17 @@ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", "license": "MIT" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "tslib": "^2.4.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4361,15 +2851,6 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz", - "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -4406,12 +2887,6 @@ "@types/react": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -4756,37 +3231,42 @@ } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.21.3" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@rolldown/pluginutils": "1.0.0-rc.7" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, "node_modules/@xmldom/xmldom": { @@ -5134,6 +3614,22 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5294,18 +3790,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5354,20 +3838,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5430,13 +3900,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -5591,15 +4054,6 @@ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", "license": "MIT" }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", @@ -5647,18 +4101,6 @@ "node": ">=12" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", @@ -5728,12 +4170,18 @@ } }, "node_modules/dashjs": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.0.tgz", - "integrity": "sha512-FilZfs+0pj9NB7q2VMT4zahG+V2JoleVl6K9kWunvndICdclw/jLAfLImcmCr1WqxH4hsgsFXvaVgea9XGkgVQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.1.tgz", + "integrity": "sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA==", "license": "BSD-3-Clause", "dependencies": { - "@svta/common-media-library": "^0.17.1", + "@svta/cml-608": "1.0.1", + "@svta/cml-cmcd": "1.0.1", + "@svta/cml-cmsd": "1.0.1", + "@svta/cml-dash": "1.0.1", + "@svta/cml-id3": "1.0.1", + "@svta/cml-request": "1.0.1", + "@svta/cml-xml": "1.0.1", "bcp-47-match": "^2.0.3", "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", @@ -5745,32 +4193,6 @@ "ua-parser-js": "^1.0.37" } }, - "node_modules/dashjs/node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -5807,12 +4229,6 @@ "node": ">=0.10.0" } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -5833,6 +4249,49 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5898,16 +4357,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -5977,21 +4426,6 @@ "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", "license": "MIT" }, - "node_modules/emoji-picker-react": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.16.1.tgz", - "integrity": "sha512-MrPX0tOCfRL3uYI4of/2GRZ7S6qS7YlacKiF78uFH84/C62vcuHE2DZyv5b4ZJMk0e06es1jjB4e31Bb+YSM8w==", - "license": "MIT", - "dependencies": { - "flairup": "1.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -6135,49 +4569,11 @@ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6399,12 +4795,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -6417,15 +4807,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", - "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -6477,18 +4858,6 @@ "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6537,72 +4906,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/firebase": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.6.0.tgz", - "integrity": "sha512-8ZD1Gcv916Qp8/nsFH2+QMIrfX/76ti6cJwxQUENLXXnKlOX/IJZaU2Y3bdYf5r1mbownrQKfnWtrt+MVgdwLA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/ai": "2.6.0", - "@firebase/analytics": "0.10.19", - "@firebase/analytics-compat": "0.2.25", - "@firebase/app": "0.14.6", - "@firebase/app-check": "0.11.0", - "@firebase/app-check-compat": "0.4.0", - "@firebase/app-compat": "0.5.6", - "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.11.1", - "@firebase/auth-compat": "0.6.1", - "@firebase/data-connect": "0.3.12", - "@firebase/database": "1.1.0", - "@firebase/database-compat": "2.1.0", - "@firebase/firestore": "4.9.2", - "@firebase/firestore-compat": "0.4.2", - "@firebase/functions": "0.13.1", - "@firebase/functions-compat": "0.4.1", - "@firebase/installations": "0.6.19", - "@firebase/installations-compat": "0.2.19", - "@firebase/messaging": "0.12.23", - "@firebase/messaging-compat": "0.2.23", - "@firebase/performance": "0.7.9", - "@firebase/performance-compat": "0.2.22", - "@firebase/remote-config": "0.7.0", - "@firebase/remote-config-compat": "0.2.20", - "@firebase/storage": "0.14.0", - "@firebase/storage-compat": "0.4.0", - "@firebase/util": "1.13.0" - } - }, - "node_modules/firebase/node_modules/@firebase/auth": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", - "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@firebase/app": "0.x", - "@react-native-async-storage/async-storage": "^1.18.1" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/flairup": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", - "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", - "license": "MIT" - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -6783,16 +5086,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6802,6 +5095,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7034,12 +5340,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, "node_modules/i18next": { "version": "25.8.13", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.13.tgz", @@ -7232,6 +5532,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7278,6 +5594,38 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7299,6 +5647,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7343,19 +5707,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7377,19 +5728,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -7486,10 +5824,15 @@ } }, "node_modules/lenis": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.17.tgz", - "integrity": "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==", + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", + "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], "funding": { "type": "github", "url": "https://github.com/sponsors/darkroomengineering" @@ -7534,6 +5877,279 @@ "immediate": "~3.0.5" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -7577,24 +6193,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7602,12 +6206,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -7630,16 +6228,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/lucide-react": { "version": "0.344.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", @@ -8732,6 +7320,27 @@ "node": ">= 6" } }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8922,9 +7531,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -9077,6 +7686,19 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/preact": { "version": "10.28.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", @@ -9133,30 +7755,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -9352,16 +7950,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-chartjs-2": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", - "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", - "license": "MIT", - "peerDependencies": { - "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -9461,15 +8049,6 @@ } } }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-input-mask": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-input-mask/-/react-input-mask-2.0.4.tgz", @@ -9505,6 +8084,15 @@ "react": ">=16.13.1" } }, + "node_modules/react-loading-skeleton": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz", + "integrity": "sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -9532,16 +8120,6 @@ "react": ">=18" } }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -9621,21 +8199,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-snowfall": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-snowfall/-/react-snowfall-2.4.0.tgz", @@ -9671,22 +8234,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9708,44 +8255,6 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, "node_modules/remark-emoji": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.2.tgz", @@ -9883,49 +8392,230 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "node_modules/rolldown": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup-plugin-visualizer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz", + "integrity": "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^11.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^18.0.0" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/run-parallel": { @@ -9951,26 +8641,6 @@ "queue-microtask": "^1.2.2" } }, - "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/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", @@ -9986,16 +8656,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -10171,6 +8831,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -10400,12 +9070,6 @@ "node": ">=0.8" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -10413,13 +9077,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -10565,6 +9229,32 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", @@ -10575,12 +9265,6 @@ "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -10827,28 +9511,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/video.js": { "version": "8.23.4", "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.4.tgz", @@ -10901,21 +9563,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -10924,23 +9588,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { "optional": true }, - "lightningcss": { + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -10957,6 +9631,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -10975,6 +9655,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -11019,35 +9712,6 @@ } } }, - "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", - "license": "Apache-2.0" - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/webworkify-webpack": { "version": "2.1.5", "resolved": "git+ssh://git@github.com/xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef", @@ -11085,51 +9749,6 @@ "node": ">=0.10.0" } }, - "node_modules/workbox-core": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", - "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", - "license": "MIT" - }, - "node_modules/workbox-precaching": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", - "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.0", - "workbox-routing": "7.4.0", - "workbox-strategies": "7.4.0" - } - }, - "node_modules/workbox-routing": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", - "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.0" - } - }, - "node_modules/workbox-strategies": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", - "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.0" - } - }, - "node_modules/workbox-window": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", - "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", - "license": "MIT", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "7.4.0" - } - }, "node_modules/workerd": { "version": "1.20260421.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260421.1.tgz", @@ -11626,23 +10245,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -11665,6 +10267,23 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", @@ -11677,45 +10296,12 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d6041e6..bed6c62 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "vite build", "build:cf": "vite build", "build:coolify": "vite build", + "build:report": "vite build && node scripts/bundle-report.mjs", "lint": "eslint .", "preview": "vite preview", "start": "node server/index.js", @@ -25,55 +26,48 @@ "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", - "@firebase/firestore": "^4.9.2", "@headlessui/react": "^2.2.0", "@hono/node-server": "^1.19.14", - "@noble/hashes": "^1.8.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", - "@react-hook/resize-observer": "^2.0.2", + "@tanstack/react-virtual": "^3.13.24", "axios": "^1.13.2", "canvas-confetti": "^1.9.4", - "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dashjs": "^5.1.0", + "dashjs": "^5.1.1", "date-fns": "^4.1.0", "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "emoji-mart": "^5.6.0", - "emoji-picker-react": "^4.12.2", "file-saver": "^2.0.5", - "firebase": "^12.6.0", "framer-motion": "11.11.10", "hls.js": "^1.6.16", "hono": "^4.12.14", "i18next": "^25.8.10", "i18next-browser-languagedetector": "^8.2.1", "jszip": "^3.10.1", - "lenis": "^1.3.17", + "lenis": "^1.3.23", "lucide-react": "^0.344.0", "mpegts.js": "^1.8.0", "pako": "^2.1.0", "qrcode": "^1.5.4", "react": "^18.3.1", - "react-chartjs-2": "^5.3.0", "react-colorful": "^5.6.1", "react-country-flag": "^3.1.0", "react-dom": "^18.3.1", "react-force-graph-2d": "^1.29.1", "react-helmet-async": "^2.0.5", "react-i18next": "^16.5.4", - "react-icons": "^5.5.0", "react-input-mask": "^2.0.4", + "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-router-dom": "^6.22.3", - "react-snowfall": "^2.3.0", - "recharts": "^2.15.3", + "react-snowfall": "^2.4.0", "remark-emoji": "^5.0.1", "remark-gfm": "^4.0.1", "shaka-player": "^5.0.0", @@ -83,12 +77,7 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "video.js": "^8.23.4", - "web-haptics": "^0.0.6", - "workbox-core": "^7.3.0", - "workbox-precaching": "^7.3.0", - "workbox-routing": "^7.3.0", - "workbox-strategies": "^7.3.0", - "workbox-window": "^7.3.0" + "web-haptics": "^0.0.6" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -98,8 +87,8 @@ "@types/react-input-mask": "^3.0.6", "@types/uuid": "^10.0.0", "@types/video.js": "^7.3.58", - "@vitejs/plugin-basic-ssl": "^1.2.0", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-basic-ssl": "^2.3.0", + "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", @@ -107,10 +96,11 @@ "eslint-plugin-unused-imports": "^4.3.0", "globals": "^15.9.0", "postcss": "^8.4.35", + "rollup-plugin-visualizer": "^7.0.1", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2", + "vite": "^8.0.11", "wrangler": "^4.15.1" }, "overrides": { diff --git a/public/sw.js b/public/sw.js index d4b0e7c..33c6dac 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,8 +5,74 @@ const DEFAULT_MIRRORS = __MOVIX_DEFAULT_MIRRORS__; const CONFIG_URL = __MOVIX_CONFIG_URL__; const NAV_TIMEOUT_MS = 3000; const CONFIG_TIMEOUT_MS = 3000; +// Ping de confirmation sur un asset statique de l'origine. Volontairement +// généreux : sur mobile, sortir de veille peut prendre 3-5s (DNS + TLS + +// radio cellulaire qui se réveille). Si même ce ping fail, l'origine est +// vraiment injoignable. +const REACHABILITY_TIMEOUT_MS = 4000; +const REACHABILITY_PROBE_PATH = '/movix.png'; const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i; +// ============================================================================ +// Image cache — TMDB images (posters, backdrops, logos) +// ============================================================================ +// +// Stratégie : cache-first sur image.tmdb.org. Premier hit = network + cache, +// hits suivants = direct depuis CacheStorage (instantané, no network). +// +// Bonus : queue de concurrence côté SW, cap à 6 simultanés. Sans throttle, +// le navigateur peut déclencher 30+ fetchs parallèles au mount du home. +// +// Bump IMAGE_CACHE_NAME pour invalider toutes les images cachées d'un coup +// (ex. quand on change la taille standard w500→w342 — sinon on continue à +// servir les vieilles URLs pendant des semaines). v2 = passage à w342 posters +// + w300 logos. +const IMAGE_CACHE_NAME = 'movix-tmdb-images-v2'; +const TMDB_IMAGE_HOST = 'image.tmdb.org'; +const MAX_CONCURRENT_IMAGE_FETCHES = 6; +let activeImageFetches = 0; +const imageFetchQueue = []; + +function acquireImageFetchSlot() { + return new Promise((resolve) => { + if (activeImageFetches < MAX_CONCURRENT_IMAGE_FETCHES) { + activeImageFetches++; + resolve(); + } else { + imageFetchQueue.push(resolve); + } + }); +} + +function releaseImageFetchSlot() { + const next = imageFetchQueue.shift(); + if (next) { + next(); + } else { + activeImageFetches = Math.max(0, activeImageFetches - 1); + } +} + +async function handleTmdbImage(req) { + const cache = await caches.open(IMAGE_CACHE_NAME); + const cached = await cache.match(req); + if (cached) return cached; + + await acquireImageFetchSlot(); + try { + const res = await fetch(req); + if (res && (res.ok || res.type === 'opaque')) { + // .clone() avant .put() : la response ne peut être consommée qu'une fois. + // .catch silently : QuotaExceededError quand storage full → on sert la + // réponse non-cachée à l'utilisateur, qui marche quand même. + cache.put(req, res.clone()).catch(() => {}); + } + return res; + } finally { + releaseImageFetchSlot(); + } +} + // ============================================================================ // Helpers fallback domain // ============================================================================ @@ -91,6 +157,15 @@ function escapeHtml(str) { .replace(/'/g, '''); } +function buildMirrorUrl(targetHost, { from, reason, error, via } = {}) { + const url = new URL(`https://${targetHost}/`); + if (from) url.searchParams.set('from', from); + if (reason) url.searchParams.set('reason', reason); + if (error) url.searchParams.set('error', String(error).slice(0, 100)); + if (via) url.searchParams.set('via', via); + return url.href; +} + function renderRedirectPage(url) { const safe = escapeHtml(url); const html = ` @@ -186,7 +261,15 @@ self.addEventListener('activate', (event) => { event.waitUntil( (async () => { const keys = await caches.keys(); - await Promise.all(keys.map((k) => caches.delete(k))); + // Préserve le cache d'images courant ; supprime tout le reste (anciennes + // versions de cache, caches légacy d'avant cette logique). Quand on bumpe + // IMAGE_CACHE_NAME (ex. v1 → v2), l'ancienne version sera supprimée ici + // automatiquement. + await Promise.all( + keys + .filter((k) => k !== IMAGE_CACHE_NAME) + .map((k) => caches.delete(k)) + ); await self.clients.claim(); })() ); @@ -236,6 +319,48 @@ self.addEventListener('notificationclick', (event) => { // Fetch — intercepte les navigations top-level pour fallback domain // ============================================================================ +// Ping de confirmation : l'origine répond-elle réellement ? Utilisé pour +// distinguer un VRAI blocage FAI (toute l'origine bloquée) d'un échec +// transient (mobile qui sort de veille, throttling de tab background, blip +// réseau). On vise un asset statique stable, cache: 'no-store' pour forcer +// un round-trip frais. +async function probeOriginOnce() { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REACHABILITY_TIMEOUT_MS); + try { + const url = new URL(REACHABILITY_PROBE_PATH, self.location.origin).href; + const res = await fetch(`${url}?_swprobe=${Date.now()}`, { + method: 'HEAD', + cache: 'no-store', + signal: controller.signal, + credentials: 'omit', + redirect: 'manual', + }); + clearTimeout(timer); + // 2xx, 3xx, 4xx = origine répond (même un 404 confirme que le serveur + // est joignable). Seul un échec réseau ou un 5xx massif = injoignable. + return res.status < 500; + } catch { + clearTimeout(timer); + return false; + } +} + +// Deux tentatives avant de conclure à un blocage. Un seul HEAD raté n'est +// pas un signal suffisant : un blip réseau ponctuel (perte de paquet, switch +// de cell tower, throttling court) peut le faire échouer. Si la 1ère échoue +// on attend 500ms et on retente — un VRAI blocage FAI est persistant et +// échouera les deux fois ; un blip transient laissera passer la 2ème. +async function isOriginReachable() { + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + return false; + } + const first = await probeOriginOnce(); + if (first) return true; + await new Promise((r) => setTimeout(r, 500)); + return await probeOriginOnce(); +} + async function handleNavigation(req) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), NAV_TIMEOUT_MS); @@ -249,21 +374,51 @@ async function handleNavigation(req) { if (typeof navigator !== 'undefined' && navigator.onLine === false) { throw err; } - return await redirectToMirror(); + // Confirmation : l'origine est-elle vraiment injoignable ? Sinon (mobile + // qui se réveille, network blip, tab throttlé), on relaie l'erreur + // d'origine et on laisse le browser gérer (retry naturel, page d'erreur). + // On ne bascule au miroir QUE si même un HEAD simple échoue. + const reachable = await isOriginReachable(); + if (reachable) { + throw err; + } + return await redirectToMirror({ + from: self.location.hostname, + reason: 'unreachable', + error: `${err.name}: ${err.message}`, + via: 'sw-fetch', + }); } } -async function redirectToMirror() { +async function redirectToMirror({ from, reason, error, via } = {}) { const mirrors = await loadMirrors(); const target = pickNextMirror(mirrors, self.location.hostname); if (!target) return render503Page(); - const redirectUrl = `https://${target}/`; + const redirectUrl = buildMirrorUrl(target, { from, reason, error, via }); return renderRedirectPage(redirectUrl); } self.addEventListener('fetch', (event) => { const req = event.request; - if (req.mode !== 'navigate' || req.method !== 'GET') return; + if (req.method !== 'GET') return; + + // 1. TMDB image cache — intercepte tous les GET sur image.tmdb.org peu + // importe l'origine du SW. Pas de garde localhost ici : le cache est utile + // aussi en dev pour éviter de re-fetcher les mêmes posters à chaque reload. + let url; + try { + url = new URL(req.url); + } catch { + return; + } + if (url.hostname === TMDB_IMAGE_HOST) { + event.respondWith(handleTmdbImage(req)); + return; + } + + // 2. Navigation fallback (logique existante — préservée) + if (req.mode !== 'navigate') return; if (isLocalHost(self.location.hostname)) return; event.respondWith(handleNavigation(req)); }); @@ -277,10 +432,21 @@ self.addEventListener('message', async (event) => { if (!data || data.type !== 'MOVIX_FORCE_REDIRECT') return; if (isLocalHost(self.location.hostname)) return; try { + // Garde-fou : la page peut compter ses erreurs de manière trop + // optimiste (burst d'API calls qui fail au réveil mobile). On confirme + // avant de rediriger — sinon on enverrait l'utilisateur sur un miroir + // alors que l'origine répond très bien. + const reachable = await isOriginReachable(); + if (reachable) return; const mirrors = await loadMirrors(); const target = pickNextMirror(mirrors, self.location.hostname); if (!target) return; - const url = `https://${target}/`; + const url = buildMirrorUrl(target, { + from: self.location.hostname, + reason: 'api-errors', + error: data.error || 'API error threshold reached', + via: 'sw-message', + }); event.source?.postMessage({ type: 'MOVIX_REDIRECT_TO', url }); } catch {} }); diff --git a/src/App.tsx b/src/App.tsx index 84eb31d..acd1a02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,10 @@ -import React, { useEffect, useLayoutEffect, useState, useRef } from 'react'; -import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigationType, useNavigate } from 'react-router-dom'; +import React, { useEffect, useLayoutEffect, useState, useRef, lazy } from 'react'; +import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigationType, useNavigate, matchPath } from 'react-router-dom'; import { Toaster } from './components/ui/sonner'; import { TooltipProvider } from './components/ui/tooltip'; import Header from './components/Header'; import Home from './pages/Home'; -import Search from './pages/Search'; -import MovieDetails from './pages/MovieDetails'; -import TVDetails from './pages/TVDetails'; -import Movies from './pages/Movies'; -import Anime from './pages/Anime'; -import TVShows from './pages/TVShows'; -import Collections from './pages/Collections'; -import CollectionDetails from './pages/CollectionDetails'; -import GenrePage from './pages/GenrePage'; -import WatchMovie from './pages/Watch/WatchMovie'; -import WatchTv from './pages/Watch/WatchTv'; -import ProviderContent from './pages/ProviderContent'; -import ProviderCatalogPage from './pages/ProviderCatalogPage'; -import RoulettePage from './pages/RoulettePage'; -import DiscordAuth from './components/DiscordAuth'; -import GoogleAuth from './components/GoogleAuth'; import DnsBlockBanner from './components/DnsBlockBanner'; -import HelpRouter from './pages/help/HelpRouter'; -import Profile from './pages/Profile'; import { AdFreePopupProvider } from './context/AdFreePopupContext'; import { SearchProvider } from './context/SearchContext'; import { AuthProvider } from './context/AuthContext'; @@ -30,67 +12,34 @@ import { AdWarningProvider } from './context/AdWarningContext'; import { VipModalProvider } from './context/VipModalContext'; import { ProfileProvider, useProfile } from './context/ProfileContext'; import { TurnstileProvider } from './context/TurnstileContext'; +import { LightModeProvider, useLightMode } from './context/LightModeContext'; -import LiveTV from './pages/LiveTV'; -import PersonDetails from './pages/PersonDetails'; -import SuggestionPage from './pages/SuggestionPage'; -import ExtensionPage from './pages/ExtensionPage'; -import AppDownloadPage from './pages/AppDownloadPage'; -import SharedListPage from './pages/SharedListPage'; -import SharedListsCatalogPage from './pages/SharedListsCatalogPage'; import NotFound from './pages/NotFound'; import 'video.js/dist/video-js.css'; import './styles/videojs-custom.css'; -import WatchAnime from './pages/Watch/WatchAnime'; -import WatchPartyCreate from './pages/WatchPartyCreate'; -import WatchPartyRoom from './pages/WatchPartyRoom'; -import WatchPartyJoin from './pages/WatchPartyJoin'; -import WatchPartyList from './pages/WatchPartyList'; import axios from 'axios'; import Footer from './components/Footer'; import CreateAccount from './pages/CreateAccount'; import LoginBip39 from './pages/LoginBip39'; -import AlertsPage from './pages/AlertsPage'; import { AlertService } from './services/alertService'; import NotificationToast from './components/NotificationToast'; import { NotificationData } from './types/alerts'; -import DMCA from './pages/DMCA'; -import AdminPage from './pages/AdminPage'; -import DownloadPage from './pages/DownloadPage'; -import DebridPage from './pages/DebridPage'; -import ProfileSelection from './pages/ProfileSelection'; -import ProfileManagement from './pages/ProfileManagement'; import RedirectPopup from './components/RedirectPopup'; -import WishboardPage from './pages/Greenlight/WishboardPage'; -import WishboardNewRequest from './pages/Greenlight/WishboardNewRequest'; -import WishboardUserRequests from './pages/Greenlight/WishboardUserRequests'; -import SubmitLinkPage from './pages/Greenlight/SubmitLinkPage'; -import VipPage from './pages/VipPage'; -import VipDonatePage from './pages/VipDonatePage'; -import VipInvoicesPage from './pages/VipInvoicesPage'; -import VipInvoicePage from './pages/VipInvoicePage'; -import VipGiftPage from './pages/VipGiftPage'; -import WhatIsMovixPage from './pages/WhatIsMovixPage'; -import Privacy from './pages/Privacy'; -import TermsOfService from './pages/TermsOfService'; +import { TopProgressBar } from './components/TopProgressBar'; import SmoothScroll from './components/SmoothScroll'; -import WrappedPage from './pages/WrappedPage'; -import CineGraphPage from './pages/CineGraph'; -import SettingsPage from './pages/SettingsPage'; -import Top10Page from './pages/Top10Page'; -import OAuthAuthorizePage from './pages/OAuthAuthorizePage'; -import FranceTVBrowse from './pages/FranceTV/FranceTVBrowse'; -import FranceTVInfo from './pages/FranceTV/FranceTVInfo'; -import FranceTVPlayer from './pages/FranceTV/FranceTVPlayer'; import AprilFoolsAdminPage from './pages/AprilFoolsAdminPage'; +import ProfileSelection from './pages/ProfileSelection'; +import { ROUTES, type RouteEntry } from './routing/registry'; +import { DelayedSuspense } from './components/DelayedSuspense'; +import { RouteProgressBar } from './components/RouteProgressBar'; import ScreenSaver from './components/ScreenSaver'; import { useIdleTimer } from './hooks/useIdleTimer'; import { startVipVerification } from './utils/vipUtils'; import { broadcastAuthChange, clearStoredAuthSession, getResolvedAccountContext } from './utils/accountAuth'; -import { isSyncableStorageKey } from './utils/syncStorage'; +import { isSyncableStorageKey, SYNC_OUTBOX_STORAGE_KEY } from './utils/syncStorage'; import i18n, { detectInitialLanguage } from './i18n'; import { useTranslation } from 'react-i18next'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion, AnimatePresence, MotionConfig } from 'framer-motion'; import IntroAnimation from './components/IntroAnimation'; import { IntroProvider, useIntro } from './context/IntroContext'; import { APRIL_FOOLS_ADMIN_PATH, isAprilFoolsAdminEnabled } from './utils/aprilFools'; @@ -519,6 +468,53 @@ const PrivateRoute = ({ children }: { children: React.ReactNode }) => { return isAuthenticated ? children : ; }; +// Cache module-level des composants Lazy par path. Sans ça, chaque appel à +// renderRouteEntry (ROUTES.map à chaque render d'App) créerait une nouvelle +// instance lazy() avec son propre cache de chunk → instabilité d'identité. +const lazyComponentCache = new Map>>(); +const getCachedLazy = (entry: RouteEntry) => { + let cached = lazyComponentCache.get(entry.path); + if (!cached) { + cached = lazy(entry.loader as () => Promise<{ default: React.ComponentType }>); + lazyComponentCache.set(entry.path, cached); + } + return cached; +}; + +// Wrapper qui injecte `key={location.pathname}` sur le composant lazy. Sans ça, +// quand l'utilisateur navigue entre deux URLs matchant le même Route pattern +// (ex. /movie/abc → /movie/xyz), React Router réutilise l'instance composant +// avec juste les params updated. Les useState de la page (movie, cast, crew, +// loading…) gardent les valeurs de l'ancien id pendant que le nouveau fetch +// tourne — l'utilisateur voit l'ancien film tant que TMDB répond pas. +// Avec key={pathname}, la clé change → React remount le composant → state reset +// → loader/skeleton affiché jusqu'au nouveau fetch. +const RouteLazyContent: React.FC<{ + Lazy: React.LazyExoticComponent>; + fallback: React.ReactNode; +}> = ({ Lazy, fallback }) => { + const location = useLocation(); + return ( + + + + ); +}; + +const renderRouteEntry = (entry: RouteEntry) => { + const Lazy = getCachedLazy(entry); + let element: React.ReactNode = ( + } + /> + ); + if (entry.guard === 'private') { + element = {element}; + } + return ; +}; + // PersistenceManager component to sync localStorage with backend (disabled for guests and VIP) const PersistenceManager = () => { const [isInitialSyncDone, setIsInitialSyncDone] = useState(false); @@ -544,6 +540,8 @@ const PersistenceManager = () => { React.useEffect(() => { (window as any).setProfileDataLoading = (loading: boolean) => { isProfileDataLoadingRef.current = loading; + const diag = (window as unknown as { __syncDiag?: Record }).__syncDiag; + if (diag) diag.gateLastSetTo = loading; debugAppLog('Profile data loading state changed:', loading); }; @@ -625,6 +623,41 @@ const PersistenceManager = () => { useEffect(() => { // Setup localStorage sync (now allowed on watch routes) + // Diagnostic counters (window.__syncDiag) — readable from remote inspector + // to pinpoint which guard silently drops sync ops on Firefox mobile. + const syncDiag = ((window as unknown as { __syncDiag?: Record }).__syncDiag = { + setItemIntercepted: 0, + removeItemIntercepted: 0, + diffsQueued: 0, + diffsDrained: 0, + skipSuppress: 0, + skipNotSyncable: 0, + skipNoop: 0, + skipForceClear: 0, + skipGateSet: 0, + skipGateRemove: 0, + skipGateEnqueue: 0, + opsEnqueued: 0, + flushGeneralCalled: 0, + flushGeneralBlockedGate: 0, + sendOpsCalled: 0, + sendOpsEmpty: 0, + sendOpsBlockedForceClear: 0, + sendOpsBlockedGate: 0, + sendOpsBlockedUserInfo: 0, + sendOpsBlockedNoToken: 0, + sendOpsAttempted: 0, + sendOpsSuccess: 0, + sendOpsError: 0, + lastSkippedKey: '', + lastUserInfo: '', + lastError: '', + gateInitialState: undefined as boolean | undefined, + gateLastSetTo: undefined as boolean | undefined, + profileLoadingExposed: typeof (window as unknown as { setProfileDataLoading?: unknown }).setProfileDataLoading === 'function' + }); + syncDiag.gateInitialState = isProfileDataLoadingRef.current; + // Initialize snapshot of current localStorage const prevValues = new Map(); for (let i = 0; i < localStorage.length; i++) { @@ -658,9 +691,10 @@ const PersistenceManager = () => { const enqueueGeneralOp = (op: any) => { // Skip sync during profile data loading (but allow on watch routes) - if (isProfileDataLoadingRef.current) return; + if (isProfileDataLoadingRef.current) { syncDiag.skipGateEnqueue++; return; } generalOpsRef.current.push(op); + syncDiag.opsEnqueued++; if (generalFlushTimeoutRef.current) return; generalFlushTimeoutRef.current = setTimeout(() => { flushGeneralOps(); @@ -669,29 +703,38 @@ const PersistenceManager = () => { }; const sendOps = async (ops: any[]) => { - if (!ops.length) return; + if (!ops.length) { syncDiag.sendOpsEmpty++; return; } + syncDiag.sendOpsCalled++; // Vérifier si un clear forcé est en cours (erreur 401) if ((window as any).__forceClearInProgress) { + syncDiag.sendOpsBlockedForceClear++; debugAppLog('Skipping sync - force clear in progress'); return; } // Skip sync during profile data loading (but allow on watch routes) if (isProfileDataLoadingRef.current) { + syncDiag.sendOpsBlockedGate++; debugAppLog('Skipping sync - profile data loading in progress'); return; } const userInfo = getUserInfo(); // Only sync for oauth and bip39 users with a selected profile - if (!userInfo.type || !userInfo.profileId || !['oauth', 'bip39'].includes(userInfo.type)) return; + if (!userInfo.type || !userInfo.profileId || !['oauth', 'bip39'].includes(userInfo.type)) { + syncDiag.sendOpsBlockedUserInfo++; + syncDiag.lastUserInfo = JSON.stringify(userInfo); + return; + } const authToken = localStorage.getItem('auth_token'); if (!authToken) { + syncDiag.sendOpsBlockedNoToken++; debugAppLog('Skipping sync - no auth token available'); return; } + syncDiag.sendOpsAttempted++; try { for (let index = 0; index < ops.length; index += MAX_SYNC_OPS_PER_REQUEST) { @@ -712,18 +755,35 @@ const PersistenceManager = () => { }); } + // NOTE: don't clear SYNC_OUTBOX_STORAGE_KEY here. The outbox holds + // un-replayed ops from previous sessions; in-session sendOps only + // sees current-session ops. The two sets are disjoint, so clearing + // here would silently drop a previous session's ops that hadn't yet + // succeeded a replay. ProfileContext.replayOutboxIfAny owns the + // outbox lifecycle; backend ops are idempotent so leaving stale + // outbox entries doesn't cause incorrect state, only one extra POST + // on next boot. + + syncDiag.sendOpsSuccess++; debugAppLog('Sync request successful'); window.dispatchEvent(new CustomEvent('sync_storage_updated')); } catch (e) { + syncDiag.sendOpsError++; + syncDiag.lastError = (e instanceof Error ? e.message : String(e)).slice(0, 200); console.error('Delta sync failed', e); } }; const flushGeneralOps = () => { + syncDiag.flushGeneralCalled++; const ops = generalOpsRef.current; generalOpsRef.current = []; // Skip sync during profile data loading (but allow on watch routes) - if (!isProfileDataLoadingRef.current && ops.length) sendOps(ops); + if (isProfileDataLoadingRef.current) { + syncDiag.flushGeneralBlockedGate++; + return; + } + if (ops.length) sendOps(ops); }; const flushProgressOps = () => { @@ -914,13 +974,13 @@ const PersistenceManager = () => { }; const processSet = (key: string, oldVal: string | null, newVal: string) => { - if (suppressSyncRef.current) return; - if (!isSyncableStorageKey(key)) return; - if (oldVal === newVal) return; + if (suppressSyncRef.current) { syncDiag.skipSuppress++; return; } + if (!isSyncableStorageKey(key)) { syncDiag.skipNotSyncable++; return; } + if (oldVal === newVal) { syncDiag.skipNoop++; return; } // Vérifier si un clear forcé est en cours (erreur 401) - if ((window as any).__forceClearInProgress) return; + if ((window as any).__forceClearInProgress) { syncDiag.skipForceClear++; return; } // Skip sync during profile data loading (but allow on watch routes) - if (isProfileDataLoadingRef.current) return; + if (isProfileDataLoadingRef.current) { syncDiag.skipGateSet++; syncDiag.lastSkippedKey = key; return; } if (isProgressKey(key)) { // Accumulate latest patch for progress keys const delta = computeObjectPatch(oldVal, newVal); @@ -953,12 +1013,12 @@ const PersistenceManager = () => { }; const processRemove = (key: string) => { - if (suppressSyncRef.current) return; - if (!isSyncableStorageKey(key)) return; + if (suppressSyncRef.current) { syncDiag.skipSuppress++; return; } + if (!isSyncableStorageKey(key)) { syncDiag.skipNotSyncable++; return; } // Vérifier si un clear forcé est en cours (erreur 401) - if ((window as any).__forceClearInProgress) return; + if ((window as any).__forceClearInProgress) { syncDiag.skipForceClear++; return; } // Skip sync during profile data loading (but allow on watch routes) - if (isProfileDataLoadingRef.current) return; + if (isProfileDataLoadingRef.current) { syncDiag.skipGateRemove++; syncDiag.lastSkippedKey = key; return; } enqueueGeneralOp({ op: 'remove', key }); if (isProgressKey(key)) { progressOpsMapRef.current.delete(key); @@ -976,6 +1036,7 @@ const PersistenceManager = () => { queueMicrotask(() => { diffQueueScheduled = false; const batch = pendingDiffs.splice(0); + syncDiag.diffsDrained += batch.length; for (const entry of batch) { if (entry.newVal === null) { processRemove(entry.key); @@ -986,8 +1047,13 @@ const PersistenceManager = () => { }); }; - // BroadcastChannel for cross-tab sync (replaces 2s/5s polling on Safari/Firefox). - // Other tabs receive { key, value } and reconcile against their own prevValuesRefLocal. + // BroadcastChannel for cross-tab sync. Receivers ONLY refresh prev so + // future local diffs are correct against shared localStorage state — they + // must NOT enqueue sync ops here. The originating tab handles its own + // sync; re-syncing in receivers races with this tab's pending 1s flush + // and can interleave a `remove` between a user's `arrayAdd X` and the + // backing `arrayAdd A,B,C,D` (when another tab's loadProfileData wipes + // and re-applies the syncable keys), losing X from the backend. const supportsBroadcastChannel = typeof BroadcastChannel !== 'undefined'; let channel: BroadcastChannel | null = null; if (supportsBroadcastChannel && isLocalStorageAvailable) { @@ -998,12 +1064,11 @@ const PersistenceManager = () => { const key = data.key; const value = data.value as string | null | undefined; if (typeof key !== 'string') return; - const oldVal = prevValuesRefLocal.current.get(key) ?? null; const newVal = (value === undefined ? null : value) as string | null; - if (oldVal !== newVal) { + if (newVal === null) { + prevValuesRefLocal.current.delete(key); + } else { prevValuesRefLocal.current.set(key, newVal); - pendingDiffs.push({ key, oldVal, newVal }); - scheduleDiffDrain(); } }; } catch (error) { @@ -1063,13 +1128,34 @@ const PersistenceManager = () => { } }, 2000) : null; // Poll every 2 seconds for Safari and Firefox/Librewolf without BroadcastChannel - const originalSetItem = localStorage.setItem; - const originalRemoveItem = localStorage.removeItem; - const originalClear = localStorage.clear; + // Patch Storage.prototype, NOT the localStorage instance. On Firefox, + // `localStorage` is a LegacyPlatformObject with a named-property setter: + // assigning `localStorage.setItem = fn` is interpreted as + // `setItem("setItem", fn.toString())` and stores the function as a + // localStorage entry — leaving the original prototype method in place, + // so writes are never intercepted and zero sync requests fire. Patching + // the prototype (a plain object, no named-property handler) works on + // every engine. The `this === localStorage` guard keeps sessionStorage + // writes on the unmodified path. + const originalSetItem = Storage.prototype.setItem; + const originalRemoveItem = Storage.prototype.removeItem; + const originalClear = Storage.prototype.clear; - localStorage.setItem = function (key: string, value: string) { + // Past versions of this code attempted `localStorage.setItem = fn` and + // accidentally seeded junk entries on Firefox users. Drop them once. + for (const junkKey of ['setItem', 'removeItem', 'clear']) { + const v = localStorage.getItem(junkKey); + if (v && v.startsWith('function')) { + originalRemoveItem.call(localStorage, junkKey); + prevValuesRefLocal.current.delete(junkKey); + } + } + + Storage.prototype.setItem = function (this: Storage, key: string, value: string) { + if (this !== localStorage) return originalSetItem.call(this, key, value); if (!isLocalStorageAvailable) return; + syncDiag.setItemIntercepted++; const oldVal = prevValuesRefLocal.current.get(key) ?? localStorage.getItem(key); originalSetItem.call(localStorage, key, value); prevValuesRefLocal.current.set(key, value); @@ -1080,13 +1166,19 @@ const PersistenceManager = () => { // Defer JSON-diff cost off the synchronous write path. if (!isProfileDataLoadingRef.current) { pendingDiffs.push({ key, oldVal, newVal: value }); + syncDiag.diffsQueued++; scheduleDiffDrain(); + } else { + syncDiag.skipGateSet++; + syncDiag.lastSkippedKey = key; } } as any; - localStorage.removeItem = function (key: string) { + Storage.prototype.removeItem = function (this: Storage, key: string) { + if (this !== localStorage) return originalRemoveItem.call(this, key); if (!isLocalStorageAvailable) return; + syncDiag.removeItemIntercepted++; const oldVal = prevValuesRefLocal.current.get(key) ?? null; originalRemoveItem.call(localStorage, key); prevValuesRefLocal.current.delete(key); @@ -1097,11 +1189,16 @@ const PersistenceManager = () => { // Defer downstream sync work into the microtask drain. if (!isProfileDataLoadingRef.current) { pendingDiffs.push({ key, oldVal, newVal: null }); + syncDiag.diffsQueued++; scheduleDiffDrain(); + } else { + syncDiag.skipGateRemove++; + syncDiag.lastSkippedKey = key; } } as any; - localStorage.clear = function () { + Storage.prototype.clear = function (this: Storage) { + if (this !== localStorage) return originalClear.call(this); if (!isLocalStorageAvailable) return; // Snapshot keys (with their previous values) before wiping localStorage. @@ -1127,16 +1224,14 @@ const PersistenceManager = () => { prevValuesRefLocal.current.clear(); } as any; + // Same rule as the BroadcastChannel handler: refresh prev only, never + // enqueue sync ops. See the channel.onmessage comment above for the + // race that re-syncing here re-introduces. const storageListener = (e: StorageEvent) => { if (!e.key) return; - // Skip sync during profile data loading (but allow on watch routes) - if (isProfileDataLoadingRef.current) return; - if (e.newValue === null) { - processRemove(e.key); prevValuesRefLocal.current.delete(e.key); } else { - processSet(e.key, e.oldValue, e.newValue); prevValuesRefLocal.current.set(e.key, e.newValue); } }; @@ -1155,6 +1250,27 @@ const PersistenceManager = () => { const flushPendingOpsSync = () => { if (isProfileDataLoadingRef.current) return; if ((window as unknown as { __forceClearInProgress?: boolean }).__forceClearInProgress) return; + + // Synchronously drain pendingDiffs FIRST. The microtask scheduled by + // the wrapped setItem/removeItem may not have run yet on Firefox: its + // unload sequencing can fire pagehide before the microtask checkpoint + // when the user refreshes immediately after a write (e.g., F5 right + // after submitting a VIP key). Without this explicit drain, in-flight + // diffs would never reach generalOpsRef and the outbox + keepalive + // path below would have nothing to flush — losing the user's write + // across reload. Chrome reliably drains microtasks before pagehide, + // which is why this manifests as "Firefox-only data loss". + if (pendingDiffs.length) { + const batch = pendingDiffs.splice(0); + for (const entry of batch) { + if (entry.newVal === null) { + processRemove(entry.key); + } else { + processSet(entry.key, entry.oldVal, entry.newVal); + } + } + } + if (generalFlushTimeoutRef.current) { clearTimeout(generalFlushTimeoutRef.current); generalFlushTimeoutRef.current = null; @@ -1180,6 +1296,61 @@ const PersistenceManager = () => { const authToken = localStorage.getItem('auth_token'); if (!authToken) return; + // Persist a recovery outbox to localStorage BEFORE the keepalive fetch. + // fetch keepalive is best-effort: on Firefox the Authorization header + // triggers a CORS preflight that the browser may drop on unload, and + // both engines cap inflight keepalive bytes. If the request never lands + // server-side, the next page load would otherwise wipe localStorage and + // restore stale backend state — losing the user's write. ProfileContext + // .replayOutboxIfAny reads this on boot and POSTs before the wipe runs. + // + // We MERGE with any existing outbox for the same user/profile rather + // than overwriting. Without merge, a chain of partial failures (replay + // 5xx → next-session writes → next unload) would silently drop the + // older un-replayed ops every cycle. A hard cap on op count prevents + // unbounded growth across many failed cycles; oldest ops are dropped + // first since newer ops reflect later state and ops are idempotent. + try { + const MAX_OUTBOX_OPS = 10000; + let mergedOps: Array> = pending; + try { + const existingRaw = localStorage.getItem(SYNC_OUTBOX_STORAGE_KEY); + if (existingRaw) { + const parsed = JSON.parse(existingRaw) as { + userType?: string; + profileId?: string; + ops?: unknown; + } | null; + if (parsed + && parsed.userType === userInfo.type + && parsed.profileId === userInfo.profileId + && Array.isArray(parsed.ops) + && parsed.ops.length > 0) { + mergedOps = [ + ...(parsed.ops as Array>), + ...pending + ]; + } + } + } catch { /* noop */ } + + if (mergedOps.length > MAX_OUTBOX_OPS) { + mergedOps = mergedOps.slice(mergedOps.length - MAX_OUTBOX_OPS); + } + + const outboxPayload = { + userType: userInfo.type, + profileId: userInfo.profileId, + userId: userInfo.id || undefined, + ops: mergedOps, + ts: Date.now() + }; + localStorage.setItem(SYNC_OUTBOX_STORAGE_KEY, JSON.stringify(outboxPayload)); + } catch (outboxErr) { + // Quota exceeded or other write error — proceed with keepalive anyway. + console.warn('[outbox] failed to persist outbox at unload:', outboxErr); + } + try { for (let index = 0; index < pending.length; index += MAX_SYNC_OPS_PER_REQUEST) { const batch = pending.slice(index, index + MAX_SYNC_OPS_PER_REQUEST); @@ -1223,9 +1394,9 @@ const PersistenceManager = () => { if (browserPollingInterval) clearInterval(browserPollingInterval); try { channel?.close(); } catch { /* noop */ } channel = null; - localStorage.setItem = originalSetItem as any; - localStorage.removeItem = originalRemoveItem as any; - localStorage.clear = originalClear as any; + Storage.prototype.setItem = originalSetItem; + Storage.prototype.removeItem = originalRemoveItem; + Storage.prototype.clear = originalClear; }; }, [isWatchRoute]); // Add isWatchRoute as dependency @@ -1571,6 +1742,20 @@ const AppWithIntro: React.FC = () => { }); }, []); + // Idle prefetch (Milestone 6): preload high-traffic route chunks shortly + // after mount via requestIdleCallback (setTimeout fallback for Safari/FF). + useEffect(() => { + const IDLE_PREFETCH = ['/movies', '/tv-shows', '/anime', '/search']; + // @ts-expect-error - requestIdleCallback not in all TS DOM lib versions + const ric = window.requestIdleCallback || ((cb: () => void) => setTimeout(cb, 1500)); + ric(() => { + for (const path of IDLE_PREFETCH) { + const entry = ROUTES.find(r => matchPath(r.path, path)); + entry?.loader({ silent: true }).catch(() => {/* swallow — best-effort prefetch */}); + } + }, { timeout: 3000 }); + }, []); + return (
{/* Intro overlay — le site charge derrière */} @@ -1602,83 +1787,27 @@ const AppWithIntro: React.FC = () => { + {/* Eager — landing page, kept in main bundle */} } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + {/* Routes spéciales avec props ou logique conditionnelle */} } /> + } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* Watch Party Routes */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - : } /> - } /> - } /> - {/* Wishboard / Greenlight Routes */} - } /> - } /> - } /> - } /> - {/* VIP Route */} - } /> - } /> - } /> - } /> - } /> - {/* What is Movix Route */} - } /> - } /> - } /> - } /> } /> - {/* CinéGraph Route */} - } /> - {/* Settings Route */} - } /> - {/* Top 10 Route */} - } /> - {/* France.tv Routes */} - } /> - } /> - } /> - {/* Wrapped Route */} - } /> - } /> - {/* Route catch-all pour la page 404 */} + } /> + + : } + /> + + {/* Toutes les autres routes — depuis le registry */} + {ROUTES.map(renderRouteEntry)} + + {/* 404 — eager (frequently entered cold) */} } /> @@ -1709,7 +1838,7 @@ const EmbedBlockPage = () => ( {i18n.t('embed.message')}

void }) => (
); +// Wraps the tree in a tied to the Mode léger / animation prefs. +// When `transitions` is disabled (manually or because Mode léger is on), +// framer-motion treats EVERY animation as if `prefers-reduced-motion: reduce` +// were set — initial/animate/exit are skipped on transform/opacity for free. +const AnimationMotionConfig: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { effectivePrefs } = useLightMode(); + return ( + + {children} + + ); +}; + function App() { const [forceContinue, setForceContinue] = React.useState(false); @@ -1763,6 +1905,8 @@ function App() { return ( + + @@ -1773,6 +1917,7 @@ function App() { + @@ -1783,6 +1928,8 @@ function App() { + + ); diff --git a/src/components/AdFreePlayerAds.tsx b/src/components/AdFreePlayerAds.tsx index e1491b5..6dd02df 100644 --- a/src/components/AdFreePlayerAds.tsx +++ b/src/components/AdFreePlayerAds.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useEffect } from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { Link } from "react-router-dom"; +import { PrefetchLink as Link } from '@/routing/PrefetchLink'; import { Play, ShieldAlert, Settings, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useAdFreePopup } from "../context/AdFreePopupContext"; diff --git a/src/components/AdminComments.tsx b/src/components/AdminComments.tsx index b28de7f..a3b4d0a 100644 --- a/src/components/AdminComments.tsx +++ b/src/components/AdminComments.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { PrefetchLink as Link } from '@/routing/PrefetchLink'; import { motion, AnimatePresence } from 'framer-motion'; import { Search, Trash2, Loader2, Film, Tv, RefreshCw, MessageSquare, AlertCircle, Sparkles, diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 74d0adb..332deaa 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -8,6 +8,7 @@ import { Link2, ListOrdered, MessageSquare, + Plug, ShieldCheck, Sparkles, Sprout, @@ -18,6 +19,7 @@ import AdminComments from './AdminComments'; import AdminHelpFeedback from './AdminHelpFeedback'; import AdminLinkSubmissions from './Greenlight/AdminLinkSubmissions'; import AdminWishboard from './Greenlight/AdminWishboard'; +import AdminOAuthApps from './AdminOAuthApps'; import AdminReports from './AdminReports'; import AdminSharedLists from './AdminSharedLists'; import StreamingLinksManager from './StreamingLinksManager'; @@ -30,6 +32,7 @@ type AdminSection = | 'links' | 'vip-keys' | 'vip-invoices' + | 'oauth-apps' | 'wishboard' | 'link-submissions' | 'comments' @@ -80,6 +83,14 @@ const AdminDashboard: React.FC = ({ role }) => { accent: 'text-yellow-300', highlight: '234 179 8' }, + { + id: 'oauth-apps', + title: t('adminOauthApps.cardTitle'), + description: t('adminOauthApps.cardDesc'), + icon: Plug, + accent: 'text-purple-300', + highlight: '168 85 247' + }, { id: 'wishboard', title: t('admin.wishboardGreenlight'), @@ -305,6 +316,16 @@ const AdminDashboard: React.FC = ({ role }) => { )} + + {activeSection === 'oauth-apps' && role === 'admin' && ( +
+

+ + {t('adminOauthApps.cardTitle')} +

+ +
+ )} )} diff --git a/src/components/AdminLogin.tsx b/src/components/AdminLogin.tsx deleted file mode 100644 index ce6d149..0000000 --- a/src/components/AdminLogin.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { verifyAdminCode, isAdminAuthenticated, logoutAdmin } from '../services/adminService'; -import { Lock, Unlock, LogOut } from 'lucide-react'; -import { checkDiscordMembership } from '../utils/discord'; -import { DISCORD_CONFIG } from '../config/discord'; - -interface AdminLoginProps { - onAdminStatusChange?: (isAdmin: boolean) => void; -} - -const AdminLogin: React.FC = ({ onAdminStatusChange }) => { - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - const [adminCode, setAdminCode] = useState(''); - const [isAdmin, setIsAdmin] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isLoggingOut, setIsLoggingOut] = useState(false); - const [error, setError] = useState(''); - - // Check if user is already authenticated as admin - useEffect(() => { - // Ignorer la vérification si on est en train de se déconnecter - if (isLoggingOut) return; - - const checkAdminStatus = async () => { - console.log("Vérification du statut admin au chargement"); - - // Check if authenticated via Discord - const isDiscordAuth = localStorage.getItem('discord_auth') === 'true'; - if (isDiscordAuth) { - try { - const discordUser = JSON.parse(localStorage.getItem('discord_user') || '{}'); - - // Si l'utilisateur est déjà identifié comme admin via Discord, conserver son statut - if (discordUser.isAdmin) { - console.log("User is already admin via Discord role (cached)"); - setIsAdmin(true); - if (onAdminStatusChange) { - onAdminStatusChange(true); - } - return; - } - - // Check if we need to refresh the role info - const lastCheck = parseInt(localStorage.getItem('discord_last_check') || '0'); - const now = Date.now(); - const needsRefresh = now - lastCheck > (DISCORD_CONFIG.CACHE_DURATION * 1000); - - if (needsRefresh) { - console.log("Refreshing Discord roles..."); - const accessToken = localStorage.getItem('discord_token'); - if (accessToken) { - const membershipData = await checkDiscordMembership(accessToken); - - // Ne pas modifier l'état si on est rate limited et qu'on n'a pas de données valides - if (membershipData.isRateLimited && !membershipData.isAdmin) { - console.log("Rate limited, preserving current admin status"); - return; - } - - const isDiscordAdmin = membershipData.isAdmin; - - // Update the user info in localStorage - const updatedUser = { - ...discordUser, - roles: membershipData.roles, - isAdmin: isDiscordAdmin - }; - localStorage.setItem('discord_user', JSON.stringify(updatedUser)); - localStorage.setItem('discord_last_check', now.toString()); - - if (isDiscordAdmin) { - console.log("User is admin via Discord role (fresh check)"); - setIsAdmin(true); - if (onAdminStatusChange) { - onAdminStatusChange(true); - } - return; - } - } - } - } catch (error) { - console.error("Error checking Discord admin status:", error); - // En cas d'erreur, on conserve le statut admin actuel si l'utilisateur l'était déjà - if (isAdmin) { - console.log("Error during Discord check, preserving admin status"); - return; - } - } - } - - // Fallback to traditional admin authentication - const adminStatus = await isAdminAuthenticated(); - console.log(`Statut admin traditionnel: ${adminStatus}`); - setIsAdmin(adminStatus); - if (onAdminStatusChange) { - onAdminStatusChange(adminStatus); - } - }; - - checkAdminStatus(); - }, [onAdminStatusChange, isLoggingOut, isAdmin]); - - const handleAdminLogin = async () => { - if (!adminCode.trim()) { - setError(t('admin.enterAdminCode')); - return; - } - - setIsLoading(true); - setError(''); - - try { - console.log(`Tentative de connexion avec le code: ${adminCode}`); - const isValid = await verifyAdminCode(adminCode); - console.log(`Résultat de la vérification: ${isValid}`); - - if (isValid) { - setIsAdmin(true); - setIsOpen(false); - setAdminCode(''); - if (onAdminStatusChange) { - onAdminStatusChange(true); - } - } else { - setError(t('admin.invalidAdminCode')); - } - } catch (err) { - console.error('Erreur complète:', err); - setError(t('admin.codeVerificationError')); - } finally { - setIsLoading(false); - } - }; - - const handleLogout = async () => { - setIsLoggingOut(true); - - try { - await logoutAdmin(); - // Attendre un court instant pour s'assurer que le localStorage est bien mis à jour - setTimeout(() => { - setIsAdmin(false); - if (onAdminStatusChange) { - onAdminStatusChange(false); - } - setIsLoggingOut(false); - }, 100); - } catch (error) { - console.error('Erreur lors de la déconnexion:', error); - setIsLoggingOut(false); - } - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleAdminLogin(); - } - }; - - // Vérifier si on doit afficher le bouton Admin basé sur les rôles Discord - const shouldShowAdminButton = () => { - // Si déjà authentifié comme admin, on affiche toujours le bouton - if (isAdmin) return true; - - // Vérifier l'authentification Discord - const isDiscordAuth = localStorage.getItem('discord_auth') === 'true'; - if (isDiscordAuth) { - try { - const discordUser = JSON.parse(localStorage.getItem('discord_user') || '{}'); - return discordUser.isAdmin || false; - } catch { - return false; - } - } - - // Si l'utilisateur n'est pas connecté via Discord, ne pas afficher le bouton - return false; - }; - - // Si on ne doit pas afficher le bouton, retourner null - if (!shouldShowAdminButton()) { - return null; - } - - return ( -
- {isAdmin ? ( - - ) : ( - - )} - - {isOpen && !isAdmin && ( -
-

{t('admin.adminLogin')}

-
-
- setAdminCode(e.target.value)} - onKeyPress={handleKeyPress} - placeholder={t('admin.adminCode')} - className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - autoFocus - /> -
- {error &&

{error}

} - -
-
- )} -
- ); -}; - -export default AdminLogin; diff --git a/src/components/AdminOAuthApps.tsx b/src/components/AdminOAuthApps.tsx new file mode 100644 index 0000000..ae628dc --- /dev/null +++ b/src/components/AdminOAuthApps.tsx @@ -0,0 +1,1073 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import axios, { AxiosError } from 'axios'; +import { AnimatePresence, motion } from 'framer-motion'; +import { toast } from 'sonner'; +import { useTranslation } from 'react-i18next'; +import { + ArrowUpFromLine, + BarChart3, + Coins, + Copy, + Image as ImageIcon, + KeyRound, + Pencil, + Plus, + RefreshCw, + ShieldCheck, + Trash2, + Upload, + X, +} from 'lucide-react'; + +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { Checkbox } from './ui/checkbox'; +import ConfirmDialog from './ui/confirm-dialog'; +import { Input } from './ui/input'; +import ReusableModal from './ui/reusable-modal'; +import { Switch } from './ui/switch'; +import { Textarea } from './ui/textarea'; + +interface OAuthAppRow { + id: number; + clientId: string; + clientName: string; + description: string | null; + homepageUrl: string | null; + redirectUris: string[]; + allowedScopes: string[]; + publicClient: boolean; + requirePkce: boolean; + hasClientSecret: boolean; + iconFilename: string | null; + iconUrl: string | null; + vipDaysBalance: number; + isActive: boolean; + createdAt: number; + updatedAt: number; + stats30d?: Record; +} + +interface AppStats { + sinceMs: number; + byType: { event_type: string; n: number }[]; + byDay: { day: string; n: number }[]; + uniqueUsers: number; +} + +interface AppGrant { + id: number; + clientId: string; + userId: string; + userType: string; + userIdOnly: string; + daysGranted: number; + accessKeyHint: string | null; + expiresAt: string | null; + grantedAt: number; + revokedAt: number | null; +} + +const API_URL = import.meta.env.VITE_MAIN_API; + +function getRequestErrorMessage(error: unknown): string { + if (axios.isAxiosError(error)) { + const ax = error as AxiosError<{ error?: string }>; + return ax.response?.data?.error || ax.message; + } + if (error instanceof Error) return error.message; + return ''; +} + +function fmtDate(ms: number | null): string { + if (!ms) return '—'; + try { + return new Date(ms).toLocaleString(); + } catch { + return '—'; + } +} + +const AdminOAuthApps: React.FC = () => { + const { t } = useTranslation(); + const [apps, setApps] = useState([]); + const [scopes, setScopes] = useState([]); + const [loading, setLoading] = useState(true); + const [includeInactive, setIncludeInactive] = useState(false); + const [search, setSearch] = useState(''); + + const [createOpen, setCreateOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [statsFor, setStatsFor] = useState(null); + const [statsData, setStatsData] = useState(null); + const [grantsData, setGrantsData] = useState([]); + const [statsLoading, setStatsLoading] = useState(false); + const [deleteCandidate, setDeleteCandidate] = useState(null); + const [deleting, setDeleting] = useState(false); + + const getAuth = () => ({ Authorization: `Bearer ${localStorage.getItem('auth_token')}` }); + + const loadApps = useCallback(async () => { + setLoading(true); + try { + const res = await axios.get(`${API_URL}/api/admin/oauth-apps`, { + params: { inactive: includeInactive ? '1' : undefined }, + headers: getAuth(), + }); + setApps(res.data?.apps || []); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setLoading(false); + } + }, [includeInactive, t]); + + const loadScopes = useCallback(async () => { + try { + const res = await axios.get(`${API_URL}/api/admin/oauth-apps/scopes`, { headers: getAuth() }); + setScopes(res.data?.scopes || []); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } + }, [t]); + + useEffect(() => { loadApps(); loadScopes(); }, [loadApps, loadScopes]); + + const filtered = useMemo(() => { + if (!search.trim()) return apps; + const q = search.toLowerCase(); + return apps.filter((a) => + a.clientId.toLowerCase().includes(q) + || a.clientName.toLowerCase().includes(q) + || (a.description?.toLowerCase().includes(q) ?? false), + ); + }, [apps, search]); + + const handleDeleteConfirmed = async () => { + if (!deleteCandidate) return; + setDeleting(true); + try { + await axios.delete(`${API_URL}/api/admin/oauth-apps/${deleteCandidate.clientId}`, { headers: getAuth() }); + toast.success(t('adminOauthApps.deleted')); + setDeleteCandidate(null); + loadApps(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setDeleting(false); + } + }; + + const handleToggleActive = async (app: OAuthAppRow) => { + try { + await axios.put( + `${API_URL}/api/admin/oauth-apps/${app.clientId}`, + { isActive: !app.isActive }, + { headers: getAuth() }, + ); + toast.success(app.isActive ? t('adminOauthApps.disabled') : t('adminOauthApps.enabled')); + loadApps(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } + }; + + const handleShowStats = async (app: OAuthAppRow) => { + setStatsFor(app); + setStatsLoading(true); + setStatsData(null); + setGrantsData([]); + try { + const [s, g] = await Promise.all([ + axios.get(`${API_URL}/api/admin/oauth-apps/${app.clientId}/stats`, { headers: getAuth() }), + axios.get(`${API_URL}/api/admin/oauth-apps/${app.clientId}/grants`, { headers: getAuth() }), + ]); + setStatsData({ + sinceMs: s.data?.sinceMs, + byType: s.data?.byType || [], + byDay: s.data?.byDay || [], + uniqueUsers: s.data?.uniqueUsers || 0, + }); + setGrantsData(g.data?.grants || []); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setStatsLoading(false); + } + }; + + return ( +
+
+
+ setSearch(e.target.value)} + placeholder={t('adminOauthApps.searchPlaceholder')} + className="max-w-md" + /> + +
+
+ + +
+
+ + {loading ? ( +
{t('common.loading')}
+ ) : filtered.length === 0 ? ( +
+ {t('adminOauthApps.empty')} +
+ ) : ( +
+ {filtered.map((app) => ( + setEditing(app)} + onStats={() => handleShowStats(app)} + onDelete={() => setDeleteCandidate(app)} + onToggleActive={() => handleToggleActive(app)} + onChanged={loadApps} + /> + ))} +
+ )} + + {createOpen && ( + setCreateOpen(false)} + onCreated={() => { setCreateOpen(false); loadApps(); }} + /> + )} + + {editing && ( + setEditing(null)} + onSaved={() => { setEditing(null); loadApps(); }} + /> + )} + + {statsFor && ( + { setStatsFor(null); setStatsData(null); setGrantsData([]); }} + /> + )} + + !deleting && setDeleteCandidate(null)} + /> +
+ ); +}; + +// ─── Card ─────────────────────────────────────────────────────────────── + +interface AppCardProps { + app: OAuthAppRow; + onEdit: () => void; + onStats: () => void; + onDelete: () => void; + onToggleActive: () => void; + onChanged: () => void; +} + +const AppCard: React.FC = ({ app, onEdit, onStats, onDelete, onToggleActive, onChanged }) => { + const { t } = useTranslation(); + const fileInputRef = useRef(null); + const [balanceDelta, setBalanceDelta] = useState(30); + const [busy, setBusy] = useState(false); + const [iconHover, setIconHover] = useState(false); + // Drag & drop state. On compte les enter/leave (dragCounter) parce qu'au + // passage de la souris entre 2 child elements, dragenter et dragleave se + // déclenchent en cascade ; un simple booléen flickerait. + const [isDragging, setIsDragging] = useState(false); + const dragCounter = useRef(0); + const [iconDeleteOpen, setIconDeleteOpen] = useState(false); + const [regenOpen, setRegenOpen] = useState(false); + + const getAuth = () => ({ Authorization: `Bearer ${localStorage.getItem('auth_token')}` }); + + const handleIconUpload = async (file: File) => { + if (file.size > 256 * 1024) { + toast.error(t('adminOauthApps.iconTooLarge')); + return; + } + if (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type)) { + toast.error(t('adminOauthApps.iconTypeInvalid')); + return; + } + setBusy(true); + try { + const reader = new FileReader(); + const dataBase64: string = await new Promise((resolve, reject) => { + reader.onload = () => { + const result = reader.result as string; + const comma = result.indexOf(','); + resolve(comma >= 0 ? result.slice(comma + 1) : result); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + await axios.post( + `${API_URL}/api/admin/oauth-apps/${app.clientId}/icon`, + { mimeType: file.type, dataBase64 }, + { headers: getAuth() }, + ); + toast.success(t('adminOauthApps.iconUploaded')); + onChanged(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setBusy(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + + const handleIconDeleteConfirmed = async () => { + setBusy(true); + try { + await axios.delete(`${API_URL}/api/admin/oauth-apps/${app.clientId}/icon`, { headers: getAuth() }); + toast.success(t('adminOauthApps.iconRemoved')); + setIconDeleteOpen(false); + onChanged(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setBusy(false); + } + }; + + const handleBalanceUpdate = async () => { + if (!Number.isInteger(balanceDelta) || balanceDelta === 0) { + toast.error(t('adminOauthApps.balanceDeltaInvalid')); + return; + } + setBusy(true); + try { + const res = await axios.post( + `${API_URL}/api/admin/oauth-apps/${app.clientId}/vip-balance`, + { delta: balanceDelta }, + { headers: getAuth() }, + ); + toast.success(t('adminOauthApps.balanceUpdated', { newBalance: res.data?.newBalance })); + onChanged(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setBusy(false); + } + }; + + const handleRegenerateSecretConfirmed = async () => { + setBusy(true); + try { + const res = await axios.post( + `${API_URL}/api/admin/oauth-apps/${app.clientId}/regenerate-secret`, + {}, + { headers: getAuth() }, + ); + const newSecret = res.data?.clientSecret; + if (newSecret) { + await navigator.clipboard.writeText(newSecret); + toast.success(t('adminOauthApps.secretCopied')); + } + setRegenOpen(false); + onChanged(); + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setBusy(false); + } + }; + + // ─── Drag & drop handlers (sur toute la carte) ───────────────────────── + const handleDragEnter = (e: React.DragEvent) => { + if (busy) return; + if (!e.dataTransfer?.types?.includes('Files')) return; + e.preventDefault(); + dragCounter.current += 1; + setIsDragging(true); + }; + + const handleDragOver = (e: React.DragEvent) => { + if (busy) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + }; + + const handleDragLeave = (e: React.DragEvent) => { + if (busy) return; + e.preventDefault(); + dragCounter.current = Math.max(0, dragCounter.current - 1); + if (dragCounter.current === 0) setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + if (busy) return; + e.preventDefault(); + dragCounter.current = 0; + setIsDragging(false); + const file = e.dataTransfer?.files?.[0]; + if (file) handleIconUpload(file); + }; + + const totalCalls30d = useMemo(() => { + const stats = app.stats30d || {}; + return Object.values(stats).reduce((sum, n) => sum + n, 0); + }, [app.stats30d]); + + return ( + + {/* Overlay drop : couvre toute la carte avec une animation drop-zone. */} + + {isDragging && ( + + + + + {t('adminOauthApps.dropToUpload')} + + + + )} + + +
+
setIconHover(true)} + onMouseLeave={() => setIconHover(false)} + > + {app.iconUrl ? ( + {app.clientName} + ) : ( +
+ +
+ )} + {/* Overlay hover : clic = upload */} + + {/* Petite croix rouge en haut à droite : visible au hover si une icône existe. */} + + {app.iconUrl && iconHover && !busy && ( + { e.stopPropagation(); setIconDeleteOpen(true); }} + className="absolute -right-1.5 -top-1.5 z-20 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-white shadow-lg ring-2 ring-black/60 transition-colors hover:bg-red-400" + title={t('adminOauthApps.removeIcon')} + aria-label={t('adminOauthApps.removeIcon')} + > + + + )} + + { + const f = e.target.files?.[0]; + if (f) handleIconUpload(f); + }} + /> +
+
+
+

{app.clientName}

+ {!app.isActive && ( + + {t('adminOauthApps.inactive')} + + )} + {app.publicClient ? ( + PKCE + ) : ( + Confidential + )} +
+
+ {app.clientId} + +
+ {app.description && ( +

{app.description}

+ )} +
+
+ +
+
+
+ + {t('adminOauthApps.callsLast30d')} +
+
{totalCalls30d}
+
+
+
+ + {t('adminOauthApps.vipBalance')} +
+
{app.vipDaysBalance}
+
+
+ +
+ setBalanceDelta(Number(e.target.value))} + className="h-8 w-24" + /> + + + {t('adminOauthApps.balanceHint')} + +
+ +
+ {app.allowedScopes.slice(0, 6).map((s) => ( + {s} + ))} + {app.allowedScopes.length > 6 && ( + +{app.allowedScopes.length - 6} + )} +
+ +
+ + + {!app.publicClient && ( + + )} + + +
+ + !busy && setIconDeleteOpen(false)} + /> + + !busy && setRegenOpen(false)} + /> +
+ ); +}; + +// ─── Create modal ─────────────────────────────────────────────────────── + +interface CreateAppModalProps { + scopes: string[]; + onClose: () => void; + onCreated: () => void; +} + +const CreateAppModal: React.FC = ({ scopes, onClose, onCreated }) => { + const { t } = useTranslation(); + const [clientId, setClientId] = useState(''); + const [clientName, setClientName] = useState(''); + const [description, setDescription] = useState(''); + const [homepageUrl, setHomepageUrl] = useState(''); + const [redirectUris, setRedirectUris] = useState(''); + const [selectedScopes, setSelectedScopes] = useState>(new Set()); + const [publicClient, setPublicClient] = useState(true); + const [busy, setBusy] = useState(false); + const [createdSecret, setCreatedSecret] = useState(null); + + const getAuth = () => ({ Authorization: `Bearer ${localStorage.getItem('auth_token')}` }); + + const handleCreate = async () => { + setBusy(true); + try { + const res = await axios.post( + `${API_URL}/api/admin/oauth-apps`, + { + clientId: clientId.trim().toLowerCase(), + clientName: clientName.trim(), + description: description.trim() || undefined, + homepageUrl: homepageUrl.trim() || undefined, + redirectUris: redirectUris.split('\n').map((u) => u.trim()).filter(Boolean), + allowedScopes: Array.from(selectedScopes), + publicClient, + requirePkce: publicClient, + }, + { headers: getAuth() }, + ); + if (res.data?.clientSecret) { + setCreatedSecret(res.data.clientSecret); + } else { + toast.success(t('adminOauthApps.created')); + onCreated(); + } + } catch (err) { + toast.error(getRequestErrorMessage(err) || t('common.error')); + } finally { + setBusy(false); + } + }; + + return ( + + {createdSecret ? ( +
+
+

{t('adminOauthApps.secretNotShownAgain')}

+
+
+ {createdSecret} + +
+
+ +
+
+ ) : ( +
+ + setClientId(e.target.value)} + placeholder="mon-app" + className="font-mono" + /> + + + setClientName(e.target.value)} + placeholder={t('adminOauthApps.clientNamePlaceholder')} + /> + + +