From d92f83c5a86dc49a6f78776f961e5621043a1e47 Mon Sep 17 00:00:00 2001 From: harlan Date: Fri, 28 Mar 2025 20:34:03 +1100 Subject: [PATCH 01/11] chore: wip --- cloudflare-bot-score.md | 108 ++++++++++ src/module.ts | 1 + src/runtime/server/plugins/botTracker.ts | 201 ++++++++++++++++++ src/runtime/server/routes/__robots__/debug.ts | 1 + src/runtime/types.ts | 14 ++ 5 files changed, 325 insertions(+) create mode 100644 cloudflare-bot-score.md create mode 100644 src/runtime/server/plugins/botTracker.ts diff --git a/cloudflare-bot-score.md b/cloudflare-bot-score.md new file mode 100644 index 00000000..121fbec6 --- /dev/null +++ b/cloudflare-bot-score.md @@ -0,0 +1,108 @@ +The Heuristics engine processes all requests. Cloudflare conducts a number of heuristic checks to identify automated traffic, and requests are matched against a growing database of malicious fingerprints. + +The Heuristics engine immediately gives automated requests a score of 1. + +Machine learning +The Machine Learning (ML) engine accounts for the majority of all detections, human and bot. + +This approach leverages our global network, which proxies billions of requests daily, to identify both automated and human traffic. We constantly train the ML engine to become more accurate and adapt to new threats. Most importantly, this engine learns from traffic across all Cloudflare domains and uses these insights to score traffic while honoring our strict privacy standards ↗. + +The ML engine produces scores 2 through 99. + +Anomaly detection +The Anomaly Detection (AD) engine is an optional detection engine that uses a form of unsupervised learning. Cloudflare records a baseline of your domain's traffic and uses the baseline to intelligently detect outlier requests. This approach is user agent-agnostic and can be turned on or off by your account team. + +Cloudflare does not recommend AD for domains that use Cloudflare for SaaS or expect large amounts of API traffic. The AD engine immediately gives automated requests a score of one. + +JavaScript detections +The JavaScript Detections (JSD) engine identifies headless browsers and other malicious fingerprints. This engine performs a lightweight, invisible JavaScript injection on the client side of any request while honoring our strict privacy standards ↗. We do not collect any personally identifiable information during the process. The JSD engine either blocks, challenges, or passes requests to other engines. + +JSD is enabled by default but completely optional. To adjust your settings, open the Bot Management Configuration page from Security > Bots. + +Cloudflare service +Cloudflare Service is a special bot score source for Enterprise Zero Trust to avoid false positives. + +Not computed +A bot score of 0 means Bot Management did not run on the request. Cloudflare does not run Bot Management on internal service requests that Bot Management has no interest in blocking. + +Notes on detection +Cloudflare uses the __cf_bm cookie to smooth out the bot score and reduce false positives for actual user sessions. + +The Bot Management cookie measures a single user's request pattern and applies it to the machine learning data to generate a reliable bot score for all of that user's requests. + +For more details, refer to Cloudflare Cookies. + + +JavaScript detections are another method that help Cloudflare identify bot requests. + +What are JavaScript detections? +These detections are implemented via a lightweight, invisible JavaScript code snippet that follows Cloudflare’s privacy standards ↗. JavaScript is injected only in response to requests for HTML pages or page views, excluding AJAX calls. API and mobile app traffic is unaffected. JavaScript detections have a lifespan of 15 minutes. However, the code is injected again before the session expires. After page load, the script is deferred and utilizes a separate thread (where available) to ensure that performance impact is minimal. + +The snippets of JavaScript will contain a source pointing to the challenge platform, with paths that start with /cdn-cgi/challenge-platform/... + +Note + +The information in JavaScript detections which populates js_detection.passed is stored in the cf_clearance cookie. + +Enable JavaScript detections +For Free customers (Bot Fight Mode), JavaScript detections are automatically enabled and cannot be disabled. + +For all other customers (Super Bot Fight Mode and Bot Management for Enterprise), JavaScript detections are optional. + +To enable JavaScript Detections: + +Log in to your Cloudflare dashboard ↗ and select your account and domain. +Go to Security > Bots. +Select Configure Bot Management. +For JavaScript Detections, switch the toggle to On. +For more details on how to set up bot protection, see Get started. + +Enforcing execution of JavaScript detections +Once you enable JavaScript detections, you can use the cf.bot_management.js_detection.passed field in WAF custom rules (or the request.cf.botManagement.jsDetection.passed variable in Workers). + +When adding this field to WAF custom rules, use it: + +On endpoints expecting browser traffic (avoiding native mobile applications or websocket endpoints). +After a user's first request to your application (Cloudflare needs at least one HTML request before injecting JavaScript detection). +With the Managed Challenge action, because there are legitimate reasons a user might not have passed a JavaScript detection challenge (network issues, ad blockers, disabled JavaScript in browser, native mobile apps). +Prerequisites +You must have JavaScript detections enabled on your zone. +You must have updated your Content Security Policy headers for JavaScript detections. +You must not run this field on websocket endpoints. +You must use the field in a custom rules expression that expects only browser traffic. +The action should always be a managed challenge in case a legitimate user has not received the challenge for network or browser reasons. +The path specified in the rule builder should never be the first HTML page a user visits when browsing your site. +cf.bot_management.js_detection.passed is used to indicate that a request has a Bot Management cookie present with a JavaScript detection value indicating it submitted the JavaScript detection test, and received a likely human scoring result. + +The cf.bot_management.js_detection.passed field should never be used in a WAF custom rule that matches a visitor's first request to a site. It is necessary to have at least one HTML request before Cloudflare can inject JavaScript detection. + +Example with Workers +"botManagement": { +"jsDetection": + +{ "passed": false } +, +}, + +Note + +JavaScript detections are stored in the cf_clearance cookie. + +The cf_clearance cookie cannot exceed the maximum size of 4096 bytes. + +Limitations +If you enabled Bot Management before June 2020 +Customers who enabled Enterprise Bot Management before June 2020 do not have JavaScript detections enabled by default (unless specifically requested). These customers can still enable the feature in the Cloudflare dashboard. + +If you have a Content Security Policy (CSP) +If you have a Content Security Policy (CSP), you need to take additional steps to implement JavaScript detections: + +Ensure that anything under /cdn-cgi/challenge-platform/ is allowed. Your CSP should allow scripts served from your origin domain (script-src self). +If your CSP uses a nonce for script tags, Cloudflare will add these nonces to the scripts it injects by parsing your CSP response header. +If your CSP does not use nonce for script tags and JavaScript Detection is enabled, you may see a console error such as Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-b123b8a70+4jEj+d6gWI9U6IilUJIrlnRJbRR/uQl2Jc='), or a nonce ('nonce-...') is required to enable inline execution. We highly discourage the use of unsafe-inline and instead recommend the use CSP nonces in script tags which we parse and support in our CDN. +Warning + +JavaScript detections are not supported with nonce set via tags. + +If you have ETags +Enabling JavaScript Detections (JSD) will strip ETags from HTML responses where JSD is injected. diff --git a/src/module.ts b/src/module.ts index b356bd56..968505b4 100644 --- a/src/module.ts +++ b/src/module.ts @@ -525,6 +525,7 @@ declare module 'h3' { handler: resolve('./runtime/server/middleware/injectContext'), }) addServerPlugin(resolve('./runtime/server/plugins/initContext')) + addServerPlugin(resolve('./runtime/server/plugins/botTracker')) if (isNuxtContentV2) { addServerHandler({ diff --git a/src/runtime/server/plugins/botTracker.ts b/src/runtime/server/plugins/botTracker.ts new file mode 100644 index 00000000..07d75536 --- /dev/null +++ b/src/runtime/server/plugins/botTracker.ts @@ -0,0 +1,201 @@ +import { defineNitroPlugin } from 'nitropack/runtime' +import type { DailyBotStats } from '#robots/types' + +interface BotVisit { + userAgent: string + path: string + timestamp: number + score: number +} + +// In-memory storage for bot visits +const botStats: DailyBotStats = {} + +// Bot types classification +const KNOWN_SEARCH_BOTS = [ + { pattern: 'googlebot', name: 'googlebot', reputation: 'trusted' }, + { pattern: 'bingbot', name: 'bingbot', reputation: 'trusted' }, + { pattern: 'yandexbot', name: 'yandexbot', reputation: 'trusted' }, + { pattern: 'baiduspider', name: 'baiduspider', reputation: 'trusted' }, + { pattern: 'duckduckbot', name: 'duckduckbot', reputation: 'trusted' }, + { pattern: 'slurp', name: 'yahoo', reputation: 'trusted' }, +] + +const SOCIAL_BOTS = [ + { pattern: 'twitterbot', name: 'twitter', reputation: 'trusted' }, + { pattern: 'facebookexternalhit', name: 'facebook', reputation: 'trusted' }, + { pattern: 'linkedinbot', name: 'linkedin', reputation: 'trusted' }, +] + +const SEO_BOTS = [ + { pattern: 'mj12bot', name: 'majestic12', reputation: 'neutral' }, + { pattern: 'ahrefsbot', name: 'ahrefs', reputation: 'neutral' }, + { pattern: 'semrushbot', name: 'semrush', reputation: 'neutral' }, +] + +const AI_BOTS = [ + { pattern: 'anthropic', name: 'anthropic', reputation: 'neutral' }, + { pattern: 'claude', name: 'claude', reputation: 'neutral' }, + { pattern: 'gptbot', name: 'gpt', reputation: 'neutral' }, + { pattern: 'cohere', name: 'cohere', reputation: 'neutral' }, +] + +const SUSPICIOUS_PATTERNS = [ + { pattern: 'python-requests', name: 'requests', reputation: 'suspicious' }, + { pattern: 'zgrab', name: 'zgrab', reputation: 'malicious' }, + { pattern: 'masscan', name: 'masscan', reputation: 'malicious' }, + { pattern: 'nmap', name: 'nmap', reputation: 'malicious' }, + { pattern: 'nikto', name: 'nikto', reputation: 'malicious' }, + { pattern: 'wpscan', name: 'wpscan', reputation: 'malicious' }, + { pattern: 'wget', name: 'wget', reputation: 'suspicious' }, + { pattern: 'curl', name: 'curl', reputation: 'suspicious' }, +] + +// Helper to identify bots from user agent and score them +function identifyAndScoreBot(userAgent: string, path: string): { botType: string | null, score: number } { + if (!userAgent) return { botType: null, score: 0 } + + const userAgentLower = userAgent.toLowerCase() + let score = 0 + let botType = null + let reputation = 'unknown' + + // Check for known search engines (most trusted, score 90-100) + for (const bot of KNOWN_SEARCH_BOTS) { + if (userAgentLower.includes(bot.pattern)) { + botType = bot.name + reputation = bot.reputation + score = 95 + break + } + } + + // Check for social media bots (trusted, score 80-90) + if (!botType) { + for (const bot of SOCIAL_BOTS) { + if (userAgentLower.includes(bot.pattern)) { + botType = bot.name + reputation = bot.reputation + score = 85 + break + } + } + } + + // Check for SEO tools (neutral, score 50-70) + if (!botType) { + for (const bot of SEO_BOTS) { + if (userAgentLower.includes(bot.pattern)) { + botType = bot.name + reputation = bot.reputation + score = 60 + break + } + } + } + + // Check for AI bots (varies, score 50-70) + if (!botType) { + for (const bot of AI_BOTS) { + if (userAgentLower.includes(bot.pattern)) { + botType = bot.name + reputation = bot.reputation + score = 60 + break + } + } + } + + // Check for suspicious patterns (potentially malicious, score 1-30) + if (!botType) { + for (const pattern of SUSPICIOUS_PATTERNS) { + if (userAgentLower.includes(pattern.pattern)) { + botType = pattern.name + reputation = pattern.reputation + score = reputation === 'malicious' ? 5 : 25 + break + } + } + } + + // Generic bot detection (score varies based on characteristics) + if (!botType && (userAgentLower.includes('bot') || userAgentLower.includes('spider') || userAgentLower.includes('crawler'))) { + botType = 'other-bot' + score = 40 // Default score for unknown bot + + // Adjust score based on heuristics + // Missing Accept headers often indicates a bot + if (!userAgentLower.includes('mozilla')) { + score -= 10 + } + + // Very short user agents are suspicious + if (userAgent.length < 30) { + score -= 10 + } + + // Known bad paths + if (path.includes('wp-login') || path.includes('xmlrpc.php') || path.includes('.env') || path.includes('admin')) { + score -= 15 + } + } + + return { botType, score } +} + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('request', (event) => { + const userAgent = event.node.req.headers['user-agent'] || '' + const path = event.node.req.url || '' + const { botType, score } = identifyAndScoreBot(userAgent, path) + + // Only track bot visits + if (!botType) return + + // Use waitUntil to not block the response + event.waitUntil((async () => { + try { + const now = new Date() + const dateKey = now.toISOString().split('T')[0] // YYYY-MM-DD format + + // Initialize stats for today if not exists + if (!botStats[dateKey]) { + botStats[dateKey] = { + count: 0, + bots: {} + } + } + + // Update stats + botStats[dateKey].count++ + botStats[dateKey].bots[botType] = (botStats[dateKey].bots[botType] || 0) + 1 + + // Optional: you could add a scores object to track average scores per bot type + if (!botStats[dateKey].scores) { + botStats[dateKey].scores = {} + } + + // Track average score per bot type + if (!botStats[dateKey].scores[botType]) { + botStats[dateKey].scores[botType] = { total: 0, count: 0, average: 0 } + } + + botStats[dateKey].scores[botType].total += score + botStats[dateKey].scores[botType].count++ + botStats[dateKey].scores[botType].average = + botStats[dateKey].scores[botType].total / botStats[dateKey].scores[botType].count + + // Expose stats on nitroApp + nitroApp._botStats = botStats + + // Optional: Add this bot score to the response headers for debugging + if (process.env.NODE_ENV === 'development') { + event.node.res.setHeader('X-Bot-Score', score.toString()) + event.node.res.setHeader('X-Bot-Type', botType) + } + } catch (error) { + console.error('Error tracking bot visit:', error) + } + })()) + }) +}) diff --git a/src/runtime/server/routes/__robots__/debug.ts b/src/runtime/server/routes/__robots__/debug.ts index 7c4cef78..bab69238 100644 --- a/src/runtime/server/routes/__robots__/debug.ts +++ b/src/runtime/server/routes/__robots__/debug.ts @@ -20,5 +20,6 @@ export default defineEventHandler(async (e) => { env: siteConfig.env, indexable: siteConfig.indexable, }, + botStats: e.context.nitro?.app?._botStats } }) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 3a804736..053849db 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -57,6 +57,20 @@ export interface AutoI18nConfig { strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix' } +export interface BotScoreData { + total: number + count: number + average: number +} + +export interface DailyBotStats { + [date: string]: { + count: number + bots: Record + scores?: Record + } +} + export interface RobotsContext { rule: string indexable: boolean From e0994ca5c8d423b1f245629c61f13a66f86fc27d Mon Sep 17 00:00:00 2001 From: harlan Date: Thu, 3 Apr 2025 08:16:39 +1100 Subject: [PATCH 02/11] chore: progress commit --- cloudflare-bot-score.md | 108 -- package.json | 1 + pnpm-lock.yaml | 1586 +++++++++++++++-- src/module.ts | 8 +- src/runtime/app/plugins/botd.ts | 25 + src/runtime/server/lib/is-bot/behavior.ts | 958 ++++++++++ src/runtime/server/lib/is-bot/userAgent.ts | 331 ++++ src/runtime/server/plugins/botDetection.ts | 44 + src/runtime/server/plugins/botTracker.ts | 201 --- src/runtime/server/routes/__robots__/debug.ts | 1 - src/runtime/types.ts | 3 + test/unit/botBehavior.test.ts | 488 +++++ test/unit/botDetection.test.ts | 208 +++ test/unit/botTracking.test.ts | 159 ++ 14 files changed, 3616 insertions(+), 505 deletions(-) delete mode 100644 cloudflare-bot-score.md create mode 100644 src/runtime/app/plugins/botd.ts create mode 100644 src/runtime/server/lib/is-bot/behavior.ts create mode 100644 src/runtime/server/lib/is-bot/userAgent.ts create mode 100644 src/runtime/server/plugins/botDetection.ts delete mode 100644 src/runtime/server/plugins/botTracker.ts create mode 100644 test/unit/botBehavior.test.ts create mode 100644 test/unit/botDetection.test.ts create mode 100644 test/unit/botTracking.test.ts diff --git a/cloudflare-bot-score.md b/cloudflare-bot-score.md deleted file mode 100644 index 121fbec6..00000000 --- a/cloudflare-bot-score.md +++ /dev/null @@ -1,108 +0,0 @@ -The Heuristics engine processes all requests. Cloudflare conducts a number of heuristic checks to identify automated traffic, and requests are matched against a growing database of malicious fingerprints. - -The Heuristics engine immediately gives automated requests a score of 1. - -Machine learning -The Machine Learning (ML) engine accounts for the majority of all detections, human and bot. - -This approach leverages our global network, which proxies billions of requests daily, to identify both automated and human traffic. We constantly train the ML engine to become more accurate and adapt to new threats. Most importantly, this engine learns from traffic across all Cloudflare domains and uses these insights to score traffic while honoring our strict privacy standards ↗. - -The ML engine produces scores 2 through 99. - -Anomaly detection -The Anomaly Detection (AD) engine is an optional detection engine that uses a form of unsupervised learning. Cloudflare records a baseline of your domain's traffic and uses the baseline to intelligently detect outlier requests. This approach is user agent-agnostic and can be turned on or off by your account team. - -Cloudflare does not recommend AD for domains that use Cloudflare for SaaS or expect large amounts of API traffic. The AD engine immediately gives automated requests a score of one. - -JavaScript detections -The JavaScript Detections (JSD) engine identifies headless browsers and other malicious fingerprints. This engine performs a lightweight, invisible JavaScript injection on the client side of any request while honoring our strict privacy standards ↗. We do not collect any personally identifiable information during the process. The JSD engine either blocks, challenges, or passes requests to other engines. - -JSD is enabled by default but completely optional. To adjust your settings, open the Bot Management Configuration page from Security > Bots. - -Cloudflare service -Cloudflare Service is a special bot score source for Enterprise Zero Trust to avoid false positives. - -Not computed -A bot score of 0 means Bot Management did not run on the request. Cloudflare does not run Bot Management on internal service requests that Bot Management has no interest in blocking. - -Notes on detection -Cloudflare uses the __cf_bm cookie to smooth out the bot score and reduce false positives for actual user sessions. - -The Bot Management cookie measures a single user's request pattern and applies it to the machine learning data to generate a reliable bot score for all of that user's requests. - -For more details, refer to Cloudflare Cookies. - - -JavaScript detections are another method that help Cloudflare identify bot requests. - -What are JavaScript detections? -These detections are implemented via a lightweight, invisible JavaScript code snippet that follows Cloudflare’s privacy standards ↗. JavaScript is injected only in response to requests for HTML pages or page views, excluding AJAX calls. API and mobile app traffic is unaffected. JavaScript detections have a lifespan of 15 minutes. However, the code is injected again before the session expires. After page load, the script is deferred and utilizes a separate thread (where available) to ensure that performance impact is minimal. - -The snippets of JavaScript will contain a source pointing to the challenge platform, with paths that start with /cdn-cgi/challenge-platform/... - -Note - -The information in JavaScript detections which populates js_detection.passed is stored in the cf_clearance cookie. - -Enable JavaScript detections -For Free customers (Bot Fight Mode), JavaScript detections are automatically enabled and cannot be disabled. - -For all other customers (Super Bot Fight Mode and Bot Management for Enterprise), JavaScript detections are optional. - -To enable JavaScript Detections: - -Log in to your Cloudflare dashboard ↗ and select your account and domain. -Go to Security > Bots. -Select Configure Bot Management. -For JavaScript Detections, switch the toggle to On. -For more details on how to set up bot protection, see Get started. - -Enforcing execution of JavaScript detections -Once you enable JavaScript detections, you can use the cf.bot_management.js_detection.passed field in WAF custom rules (or the request.cf.botManagement.jsDetection.passed variable in Workers). - -When adding this field to WAF custom rules, use it: - -On endpoints expecting browser traffic (avoiding native mobile applications or websocket endpoints). -After a user's first request to your application (Cloudflare needs at least one HTML request before injecting JavaScript detection). -With the Managed Challenge action, because there are legitimate reasons a user might not have passed a JavaScript detection challenge (network issues, ad blockers, disabled JavaScript in browser, native mobile apps). -Prerequisites -You must have JavaScript detections enabled on your zone. -You must have updated your Content Security Policy headers for JavaScript detections. -You must not run this field on websocket endpoints. -You must use the field in a custom rules expression that expects only browser traffic. -The action should always be a managed challenge in case a legitimate user has not received the challenge for network or browser reasons. -The path specified in the rule builder should never be the first HTML page a user visits when browsing your site. -cf.bot_management.js_detection.passed is used to indicate that a request has a Bot Management cookie present with a JavaScript detection value indicating it submitted the JavaScript detection test, and received a likely human scoring result. - -The cf.bot_management.js_detection.passed field should never be used in a WAF custom rule that matches a visitor's first request to a site. It is necessary to have at least one HTML request before Cloudflare can inject JavaScript detection. - -Example with Workers -"botManagement": { -"jsDetection": - -{ "passed": false } -, -}, - -Note - -JavaScript detections are stored in the cf_clearance cookie. - -The cf_clearance cookie cannot exceed the maximum size of 4096 bytes. - -Limitations -If you enabled Bot Management before June 2020 -Customers who enabled Enterprise Bot Management before June 2020 do not have JavaScript detections enabled by default (unless specifically requested). These customers can still enable the feature in the Cloudflare dashboard. - -If you have a Content Security Policy (CSP) -If you have a Content Security Policy (CSP), you need to take additional steps to implement JavaScript detections: - -Ensure that anything under /cdn-cgi/challenge-platform/ is allowed. Your CSP should allow scripts served from your origin domain (script-src self). -If your CSP uses a nonce for script tags, Cloudflare will add these nonces to the scripts it injects by parsing your CSP response header. -If your CSP does not use nonce for script tags and JavaScript Detection is enabled, you may see a console error such as Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-b123b8a70+4jEj+d6gWI9U6IilUJIrlnRJbRR/uQl2Jc='), or a nonce ('nonce-...') is required to enable inline execution. We highly discourage the use of unsafe-inline and instead recommend the use CSP nonces in script tags which we parse and support in our CDN. -Warning - -JavaScript detections are not supported with nonce set via tags. - -If you have ETags -Enabling JavaScript Detections (JSD) will strip ETags from HTML responses where JSD is injected. diff --git a/package.json b/package.json index 90fd720b..baa8fbd2 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@fingerprintjs/botd": "^1.9.1", "@nuxt/kit": "^3.16.0", "consola": "^3.4.0", "defu": "^6.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac4a1529..2c8bd2ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@fingerprintjs/botd': + specifier: ^1.9.1 + version: 1.9.1 '@nuxt/kit': specifier: ^3.16.0 version: 3.16.0(magicast@0.3.5) @@ -62,10 +65,10 @@ importers: version: 3.3.0(@libsql/client@0.14.0)(magicast@0.3.5)(typescript@5.6.3) '@nuxt/content-v2': specifier: npm:@nuxt/content@2.13.4 - version: '@nuxt/content@2.13.4(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))' + version: '@nuxt/content@2.13.4(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))' '@nuxt/devtools-ui-kit': specifier: ^2.2.1 - version: 2.2.1(@nuxt/devtools@2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) + version: 2.2.1(@nuxt/devtools@2.3.2(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) '@nuxt/module-builder': specifier: ^0.8.4 version: 0.8.4(@nuxt/kit@3.16.0(magicast@0.3.5))(nuxi@3.22.5)(typescript@5.6.3) @@ -80,7 +83,7 @@ importers: version: 12.1.0(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@nuxtjs/i18n': specifier: 9.3.1 - version: 9.3.1(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(rollup@3.29.5)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) + version: 9.3.1(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.34.9)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) '@unocss/nuxt': specifier: ^66.0.0 version: 66.0.0(magicast@0.3.5)(postcss@8.5.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) @@ -95,7 +98,7 @@ importers: version: 66.0.0 '@vueuse/nuxt': specifier: ^13.0.0 - version: 13.0.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + version: 13.0.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) bumpp: specifier: ^10.1.0 version: 10.1.0(magicast@0.3.5) @@ -110,7 +113,7 @@ importers: version: 6.3.2(firebase-admin@13.2.0) nuxt: specifier: ^3.16.0 - version: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + version: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) typescript: specifier: 5.6.3 version: 5.6.3 @@ -134,10 +137,10 @@ importers: version: 1.2.8 '@nuxt/devtools-kit': specifier: ^2.2.1 - version: 2.2.1(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + version: 2.2.1(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) '@nuxt/devtools-ui-kit': specifier: latest - version: 2.2.1(@nuxt/devtools@2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) + version: 2.3.2(@nuxt/devtools@2.3.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) '@nuxt/kit': specifier: ^3.16.0 version: 3.16.0(magicast@0.3.5) @@ -146,7 +149,7 @@ importers: version: 5.2.2(@nuxt/kit@3.16.0(magicast@0.3.5))(vue@3.5.13(typescript@5.6.3)) nuxt: specifier: latest - version: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + version: 3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) shiki: specifier: ^3.2.1 version: 3.2.1 @@ -161,7 +164,7 @@ importers: dependencies: nuxt: specifier: ^3.5.3 - version: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.35.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + version: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) devDependencies: '@nuxt/content': specifier: 3.0.0-alpha.9 @@ -352,6 +355,10 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} + '@cloudflare/kv-asset-handler@0.4.0': + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + engines: {node: '>=18.0.0'} + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -1021,6 +1028,9 @@ packages: '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@fingerprintjs/botd@1.9.1': + resolution: {integrity: sha512-7kv3Yolsx9E56i+L1hCEcupH5yqcI5cmVktxy6B0K7rimaH5qDXwsiA5FL+fkxeUny7XQKn7p13HvK7ofDZB3g==} + '@firebase/app-check-interop-types@0.3.3': resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} @@ -1163,22 +1173,22 @@ packages: resolution: {integrity: sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==} engines: {node: '>= 16'} - '@intlify/message-compiler@11.0.0-rc.1': - resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==} + '@intlify/message-compiler@12.0.0-alpha.2': + resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==} engines: {node: '>= 16'} '@intlify/shared@10.0.6': resolution: {integrity: sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==} engines: {node: '>= 16'} - '@intlify/shared@11.0.0-rc.1': - resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==} - engines: {node: '>= 16'} - '@intlify/shared@11.1.2': resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==} engines: {node: '>= 16'} + '@intlify/shared@12.0.0-alpha.2': + resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==} + engines: {node: '>= 16'} + '@intlify/unplugin-vue-i18n@6.0.3': resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==} engines: {node: '>= 18'} @@ -1326,6 +1336,10 @@ packages: resolution: {integrity: sha512-XXf9mNw4+fkxUzukDpJtzc32bl1+YlXZwEhc5ZgMcTbJPLpgRLDs5WWSPJ4eY/Mv1ZFvtxmMwmfgoQYVt68Qog==} engines: {node: '>=18.0.0'} + '@netlify/functions@3.0.4': + resolution: {integrity: sha512-Ox8+ABI+nsLK+c4/oC5dpquXuEIjzfTlJrdQKgQijCsDQoje7inXFAtKDLvvaGvuvE+PVpMLwQcIUL6P9Ob1hQ==} + engines: {node: '>=18.0.0'} + '@netlify/node-cookies@0.1.0': resolution: {integrity: sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==} engines: {node: ^14.16.0 || >=16.0.0} @@ -1334,6 +1348,10 @@ packages: resolution: {integrity: sha512-JkbaWFeydQdeDHz1mAy4rw+E3bl9YtbCgkntfTxq+IlNX/aIMv2/b1kZnQZcil4/sPoZGL831Dq6E374qRpU1A==} engines: {node: '>=18.0.0'} + '@netlify/serverless-functions-api@1.36.0': + resolution: {integrity: sha512-z6okREyK8in0486a22Oro0k+YsuyEjDXJt46FpgeOgXqKJ9ElM8QPll0iuLBkpbH33ENiNbIPLd1cuClRQnhiw==} + engines: {node: '>=18.0.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1355,6 +1373,11 @@ packages: engines: {node: ^16.10.0 || >=18.0.0} hasBin: true + '@nuxt/cli@3.24.0': + resolution: {integrity: sha512-D/rCodMHecznWZAxsb81lKSMhCcWIUI6TyEDQ3WIl2bTNBn4WvIYrZP3rIPJn7X4A+/CG5H8yhKDVP6Mq7XopA==} + engines: {node: ^16.10.0 || >=18.0.0} + hasBin: true + '@nuxt/content@2.13.4': resolution: {integrity: sha512-NBaHL/SNYUK7+RLgOngSFmKqEPYc0dYdnwVFsxIdrOZUoUbD8ERJJDaoRwwtyYCMOgUeFA/zxAkuADytp+DKiQ==} @@ -1386,25 +1409,49 @@ packages: peerDependencies: vite: '>=6.0' + '@nuxt/devtools-kit@2.3.2': + resolution: {integrity: sha512-K0citnz9bSecPCLl4jGfE5I5St+E9XtDmOvYqq3ranGZGZ2Mvs5RwgUkaOrn4rulvUmBGBl7Exwh5YX9PONrEQ==} + peerDependencies: + vite: '>=6.0' + '@nuxt/devtools-ui-kit@2.2.1': resolution: {integrity: sha512-ThZ/v/MJBUMp9QfLmxwCfYLPVs9WfWVvCnkrdhznsT7aG1L6GRXTZItqW25vGhy3ybBEzUbgW2pUPygBeZ5PeA==} peerDependencies: '@nuxt/devtools': 2.2.1 + '@nuxt/devtools-ui-kit@2.3.2': + resolution: {integrity: sha512-0NWADn46CnMjl/pV4eORAgs2rKY5NI+4xb0bUjK+sth774ch4PAS8wjefE1CFmAuiiEBZI8Kp5dc47spfsmsDg==} + peerDependencies: + '@nuxt/devtools': 2.3.2 + '@nuxt/devtools-wizard@2.2.1': resolution: {integrity: sha512-tJGIwFxwIOsDdpefnSPhiVJEjBC5Kr88EORV6PRYVQRPZThiO8if5UM0qhhkwoDYJ5U21nZpyIzKuCQ6svo9vA==} hasBin: true + '@nuxt/devtools-wizard@2.3.2': + resolution: {integrity: sha512-vrGjcb7O/ojrWM9FXjAyWgMLUTkb9bmQUCXc//wZw8YnJLR/hmmvo0XFwmz31BN7nMLZaMpUclROdlhRSPNf1Q==} + hasBin: true + '@nuxt/devtools@2.2.1': resolution: {integrity: sha512-JkFRYLWFoklBuf+Zv6GwS9HPOFMuN3nomApWCnsNg8H7XqlFNIvB+wetmm6+u+43bNApjqE0ne7Y//o0V6FSaA==} hasBin: true peerDependencies: vite: '>=6.0' + '@nuxt/devtools@2.3.2': + resolution: {integrity: sha512-MMx7pUW0aPDRmhe3jy91srEiFWq/Q70rjbGoHhzpVosuvyvy/fi0oKOFQqN5V4V7jJLiEx4HAoD0QdqP0I6xBA==} + hasBin: true + peerDependencies: + vite: '>=6.0' + '@nuxt/kit@3.16.0': resolution: {integrity: sha512-yPfhk58BG6wJhELkGOTCOlkMDbZkizk3IaINcyTKm+hBKiK3SheLt7S9HStNL+qZSfH2Cf7A8sYp6M72lOIEtA==} engines: {node: '>=18.12.0'} + '@nuxt/kit@3.16.1': + resolution: {integrity: sha512-Perby8hJGUeCWad5oTVXb/Ibvp18ZCUC5PxHHu+acMDmVfnxSo48yqk7qNd09VkTF3LEzoEjNZpmW2ZWN0ry7A==} + engines: {node: '>=18.12.0'} + '@nuxt/module-builder@0.8.4': resolution: {integrity: sha512-RSPRfCpBLuJtbDRaAKmc3Qzt3O98kSeRItXcgx0ZLptvROWT+GywoLhnYznRp8kbkz+6Qb5Hfiwa/RYEMRuJ4Q==} hasBin: true @@ -1416,11 +1463,20 @@ packages: resolution: {integrity: sha512-uCpcqWO6C4P5c4vi1/sq5GyajO0EOp+ZWFtPrnKaJ1pXAhA+W1aMVxAjfi2f18QMJHuRXBz1TouFg1RmWA6FuA==} engines: {node: ^14.18.0 || >=16.10.0} + '@nuxt/schema@3.16.1': + resolution: {integrity: sha512-Ri8bmT6MljpVR4DlXf9+acfgGaI4OTEdAzJU5aF2rJS78abtpnBxjXBG65kuhoL1LUlfKppDl8fTkUw5LM2JXQ==} + engines: {node: ^14.18.0 || >=16.10.0} + '@nuxt/telemetry@2.6.5': resolution: {integrity: sha512-lwMp9OHML/m0mjh7P5iz9PxINnk5smGkGebh88Wh8PjvnRooY1TBsbyq7mlSrNibpwD1BkwqhV5IAZOXWHLxMQ==} engines: {node: '>=18.12.0'} hasBin: true + '@nuxt/telemetry@2.6.6': + resolution: {integrity: sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag==} + engines: {node: '>=18.12.0'} + hasBin: true + '@nuxt/test-utils@3.17.2': resolution: {integrity: sha512-i1NiWsJx8sv8Zg8z3WD7ITehMi9s8DaR6ArgmDHaKkQ6RJSaVhrPKyGBTv3gzdoF8CHUKa3MNhdX62JWblvLMg==} engines: {node: ^18.20.5 || ^20.9.0 || ^22.0.0 || >=23.0.0} @@ -1463,6 +1519,12 @@ packages: peerDependencies: vue: ^3.3.4 + '@nuxt/vite-builder@3.16.1': + resolution: {integrity: sha512-6A/cK743xeGcoMh//Ev1HAybb5VDwovxRsNeubfuqlDxBR7WL695SAfIhEAmxpVDz8LYQBuz/NwGhTaBh7hgaQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0} + peerDependencies: + vue: ^3.3.4 + '@nuxtjs/color-mode@3.5.2': resolution: {integrity: sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA==} @@ -1598,12 +1660,18 @@ packages: '@oxc-parser/wasm@0.56.4': resolution: {integrity: sha512-UFU2VgzJlCx3K+OFvZ7fRjh8hdZdLs3mkL6xQjaRot5Mathwp/jGHy5WPt12osZeKjFDz4AIeJE9XUWPoRclTA==} + '@oxc-parser/wasm@0.60.0': + resolution: {integrity: sha512-Dkf9/D87WGBCW3L0+1DtpAfL4SrNsgeRvxwjpKCtbH7Kf6K+pxrT0IridaJfmWKu1Ml+fDvj+7HEyBcfUC/TXQ==} + '@oxc-project/types@0.53.0': resolution: {integrity: sha512-8JXXVoHnRLcl6kDBboSfAmAkKeb6PSvSc5qSJxiOFzFx0ZCLAbUDmuwR2hkBnY7kQS3LmNXaONq1BFAmwTyeZw==} '@oxc-project/types@0.56.4': resolution: {integrity: sha512-6D6CDi0otc2rGuI5vmy6uMvGB1jILy7S7bwy3IuYQ9LXraru1mB9dTdJATyVjdpUC9RgnhMymlD7JncIJkQKpA==} + '@oxc-project/types@0.60.0': + resolution: {integrity: sha512-prhfNnb3ATFHOCv7mzKFfwLij5RzoUz6Y1n525ZhCEqfq5wreCXL+DyVoq3ShukPo7q45ZjYIdjFUgjj+WKzng==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -1817,6 +1885,15 @@ packages: rollup: optional: true + '@rollup/plugin-node-resolve@16.0.1': + resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-replace@5.0.7': resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} engines: {node: '>=14.0.0'} @@ -1872,6 +1949,11 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.38.0': + resolution: {integrity: sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.34.9': resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} cpu: [arm64] @@ -1882,6 +1964,11 @@ packages: cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.38.0': + resolution: {integrity: sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.34.9': resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} cpu: [arm64] @@ -1892,6 +1979,11 @@ packages: cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.38.0': + resolution: {integrity: sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.34.9': resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} cpu: [x64] @@ -1902,6 +1994,11 @@ packages: cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.38.0': + resolution: {integrity: sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.34.9': resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} cpu: [arm64] @@ -1912,6 +2009,11 @@ packages: cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.38.0': + resolution: {integrity: sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.34.9': resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} cpu: [x64] @@ -1922,6 +2024,11 @@ packages: cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.38.0': + resolution: {integrity: sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} cpu: [arm] @@ -1932,6 +2039,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.38.0': + resolution: {integrity: sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.34.9': resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} cpu: [arm] @@ -1942,6 +2054,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.38.0': + resolution: {integrity: sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.34.9': resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} cpu: [arm64] @@ -1952,6 +2069,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.38.0': + resolution: {integrity: sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.34.9': resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} cpu: [arm64] @@ -1962,6 +2084,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.38.0': + resolution: {integrity: sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} cpu: [loong64] @@ -1972,6 +2099,11 @@ packages: cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.38.0': + resolution: {integrity: sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} cpu: [ppc64] @@ -1982,6 +2114,11 @@ packages: cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.38.0': + resolution: {integrity: sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.34.9': resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} cpu: [riscv64] @@ -1992,6 +2129,16 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.38.0': + resolution: {integrity: sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.38.0': + resolution: {integrity: sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.34.9': resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} cpu: [s390x] @@ -2002,6 +2149,11 @@ packages: cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.38.0': + resolution: {integrity: sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.34.9': resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} cpu: [x64] @@ -2012,6 +2164,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.38.0': + resolution: {integrity: sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.34.9': resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} cpu: [x64] @@ -2022,6 +2179,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.38.0': + resolution: {integrity: sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.34.9': resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} cpu: [arm64] @@ -2032,6 +2194,11 @@ packages: cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.38.0': + resolution: {integrity: sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.34.9': resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} cpu: [ia32] @@ -2042,6 +2209,11 @@ packages: cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.38.0': + resolution: {integrity: sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.34.9': resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} cpu: [x64] @@ -2052,6 +2224,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.38.0': + resolution: {integrity: sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==} + cpu: [x64] + os: [win32] + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2194,6 +2371,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/express-serve-static-core@4.19.6': resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} @@ -2429,6 +2609,11 @@ packages: peerDependencies: vue: '>=3.5.13' + '@unhead/vue@2.0.2': + resolution: {integrity: sha512-pUGcbmPNCALOVWnQRtIjJ5ubNaZus3nHfCBDPEVwhbiLzeLF6wbhgTakwksZ1EegKNOZwRAkmVbV6i+23OYEUQ==} + peerDependencies: + vue: '>=3.5.13' + '@unocss/astro@66.0.0': resolution: {integrity: sha512-GBhXT6JPqXjDXoJZTXhySk83NgOt0UigChqrUUdG4x7Z+DVYkDBION8vZUJjw0OdIaxNQ4euGWu4GDsMF6gQQg==} peerDependencies: @@ -2535,6 +2720,13 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.0.0 + '@vitejs/plugin-vue-jsx@4.1.2': + resolution: {integrity: sha512-4Rk0GdE0QCdsIkuMmWeg11gmM4x8UmTnZR/LWPm7QJ7+BsK4tq08udrN0isrrWqz5heFy9HLV/7bOLgFS8hUjA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.0.0 + '@vitejs/plugin-vue@5.2.1': resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2542,6 +2734,13 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@vitejs/plugin-vue@5.2.3': + resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitest/eslint-plugin@1.1.37': resolution: {integrity: sha512-cnlBV8zr0oaBu1Vk6ruqWzpPzFCtwY0yuwUQpNIeFOUl3UhXVwNUoOYfWkZzeToGuNBaXvIsr6/RgHrXiHXqXA==} peerDependencies: @@ -2729,6 +2928,48 @@ packages: universal-cookie: optional: true + '@vueuse/integrations@13.0.0': + resolution: {integrity: sha512-PXARslYRWf4u0xjdW6N5eC5kVQj2z/dxfZ7ildI1okLm2AwmhL+wiWzaNMSJMxTKX4ew7kNe70yJg1QjnWmE5w==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + '@vueuse/metadata@11.3.0': resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==} @@ -2992,6 +3233,13 @@ packages: peerDependencies: postcss: ^8.1.0 + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3030,6 +3278,9 @@ packages: birpc@2.2.0: resolution: {integrity: sha512-1/22obknhoj56PcE+pZPp6AbWDdY55M81/ofpPW3Ltlp9Eh4zoFFLswvZmNpRTb790CY5tsNfgbYeNOqIARJfQ==} + birpc@2.3.0: + resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3282,6 +3533,10 @@ packages: resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} engines: {node: ^14.18.0 || >=16.10.0} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -4630,6 +4885,9 @@ packages: impound@0.2.0: resolution: {integrity: sha512-gXgeSyp9Hf7qG2/PLKmywHXyQf2xFrw+mJGpoj9DsAB9L7/MIKn+DeEx98UryWXdmbv8wUUPdcQof6qXnZoCGg==} + impound@0.2.2: + resolution: {integrity: sha512-9CNg+Ly8QjH4FwCUoE9nl1zeqY1NPK1s1P6Btp4L8lJxn8oZLN/0p6RZhitnyEL0BnVWrcVPfbs0Q3x+O/ucHg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -5429,6 +5687,9 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mocked-exports@0.1.1: + resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -5482,6 +5743,16 @@ packages: xml2js: optional: true + nitropack@2.11.8: + resolution: {integrity: sha512-ummTu4R8Lhd1nO3nWrW7eeiHA2ey3ntbWFKkYakm4rcbvT6meWp+oykyrYBNFQKhobQl9CydmUWlCyztYXFPJw==} + engines: {node: ^16.11.0 || >=17.0.0} + hasBin: true + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + node-abi@3.74.0: resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} engines: {node: '>=10'} @@ -5586,6 +5857,19 @@ packages: '@types/node': optional: true + nuxt@3.16.1: + resolution: {integrity: sha512-V0odAW9Yo8s58yGnSy0RuX+rQwz0wtQp3eOgMTsh1YDDZdIIYZmAlZaLypNeieO/mbmvOOUcnuRyIGIRrF4+5A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + '@types/node': + optional: true + nypm@0.5.4: resolution: {integrity: sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==} engines: {node: ^14.16.0 || >=16.10.0} @@ -5719,6 +6003,9 @@ packages: package-manager-detector@1.0.0: resolution: {integrity: sha512-7elnH+9zMsRo7aS72w6MeRugTpdRvInmEB4Kmm9BVvPw/SLG8gXUGQ+4wF0Mys0RSWPz0B9nuBbDe8vFeA2sfg==} + package-manager-detector@1.1.0: + resolution: {integrity: sha512-Y8f9qUlBzW8qauJjd/eu6jlpJZsuPJm2ZAV0cDVd420o4EdpH5RPdoCv+60/TdJflGatr4sDfpAL6ArWZbM5tA==} + packrup@0.1.2: resolution: {integrity: sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==} @@ -6371,6 +6658,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.38.0: + resolution: {integrity: sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -6808,6 +7100,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -6972,12 +7267,18 @@ packages: unenv@2.0.0-rc.12: resolution: {integrity: sha512-aygmJLhrEnuLKDCISMoOL7ceRJeksnvXJXvtEvFei4zoOXQfvQkUGhZe8u//iK5C++M4pq3CsMbhVjFmWvOlmA==} + unenv@2.0.0-rc.15: + resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} + unhead@1.11.20: resolution: {integrity: sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==} unhead@2.0.0-rc.7: resolution: {integrity: sha512-QsA/kkx7IBRl0o03YtFdaRS+LaL5JByRXXGDabT1+08M1LCZTKFDrPKelIFVdKGrHqpO2NzoIBRocFiZML/3Sw==} + unhead@2.0.2: + resolution: {integrity: sha512-1pcK/rSA70sezpdgmupQPd/yrul8pVFJRwMvWjEthbsXoTXMqjNQlV7NBXWeWt5r2uje1lZJsvRTHF7IvdOhcg==} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} @@ -7070,6 +7371,10 @@ packages: resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} engines: {node: '>=18.12.0'} + unplugin@2.2.2: + resolution: {integrity: sha512-Qp+iiD+qCRnUek+nDoYvtWX7tfnYyXsrOnJ452FRTgOyKmTM7TUJ3l+PLPJOOWPTUyKISKp4isC5JJPSXUjGgw==} + engines: {node: '>=18.12.0'} + unstorage@1.15.0: resolution: {integrity: sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg==} peerDependencies: @@ -7222,6 +7527,11 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-node@3.1.1: + resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-checker@0.9.0: resolution: {integrity: sha512-gf/zc0KWX8ATEOgnpgAM1I+IbvWkkO80RB+FxlLtC5cabXSesbJmAUw6E+mMDDMGIT+VHAktmxJZpMTt3lSubQ==} engines: {node: '>=14.16'} @@ -7256,6 +7566,40 @@ packages: vue-tsc: optional: true + vite-plugin-checker@0.9.1: + resolution: {integrity: sha512-neH3CSNWdkZ+zi+WPt/0y5+IO2I0UAI0NX6MaXqU/KxN1Lz6np/7IooRB6VVAMBa4nigqm1GRF6qNa4+EL5jDQ==} + engines: {node: '>=14.16'} + peerDependencies: + '@biomejs/biome': '>=1.7' + eslint: '>=7' + meow: ^13.2.0 + optionator: ^0.9.4 + stylelint: '>=16' + typescript: 5.6.3 + vite: '>=2.0.0' + vls: '*' + vti: '*' + vue-tsc: ~2.2.2 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + stylelint: + optional: true + typescript: + optional: true + vls: + optional: true + vti: + optional: true + vue-tsc: + optional: true + vite-plugin-inspect@11.0.0: resolution: {integrity: sha512-Q0RDNcMs1mbI2yGRwOzSapnnA6NFO0j88+Vb8pJX0iYMw34WczwKJi3JgheItDhbWRq/CLUR0cs+ajZpcUaIFQ==} engines: {node: '>=14'} @@ -7272,6 +7616,12 @@ packages: vite: ^6.0.0 vue: ^3.5.0 + vite-plugin-vue-tracer@0.1.3: + resolution: {integrity: sha512-+fN6oo0//dwZP9Ax9gRKeUroCqpQ43P57qlWgL0ljCIxAs+Rpqn/L4anIPZPgjDPga5dZH+ZJsshbF0PNJbm3Q==} + peerDependencies: + vite: ^6.0.0 + vue: ^3.5.0 + vite@6.2.1: resolution: {integrity: sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -7312,29 +7662,69 @@ packages: yaml: optional: true - vitest-environment-nuxt@1.0.1: - resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==} - - vitest@3.0.8: - resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} + vite@6.2.4: + resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.8 - '@vitest/ui': 3.0.8 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest-environment-nuxt@1.0.1: + resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==} + + vitest@3.0.8: + resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.8 + '@vitest/ui': 3.0.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': optional: true '@vitest/ui': optional: true @@ -7585,6 +7975,10 @@ packages: resolution: {integrity: sha512-KOAmtABz17fgK+uBBJYIzaPpIgX+JgTRgY4t3zXH18akc5rRtFkRmcNTMCuSxLdbOJDY9+T/O3nyA/EQuN4EWA==} engines: {node: '>=20.6.0'} + youch-core@0.3.2: + resolution: {integrity: sha512-fusrlIMLeRvTFYLUjJ9KzlGC3N+6MOPJ68HNj/yJv2nz7zq8t4HEviLms2gkdRPUS7F5rZ5n+pYx9r88m6IE1g==} + engines: {node: '>=18'} + youch@4.1.0-beta.6: resolution: {integrity: sha512-y1aNsEeoLXnWb6pI9TvfNPIxySyo4Un3OGxKn7rsNj8+tgSquzXEWkzfA5y6gU0fvzmQgvx3JBn/p51qQ8Xg9A==} engines: {node: '>=18'} @@ -7627,7 +8021,7 @@ snapshots: '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.22.0(jiti@2.4.2)) '@eslint/markdown': 6.3.0 '@stylistic/eslint-plugin': 4.2.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@vitest/eslint-plugin': 1.1.37(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) ansis: 3.17.0 @@ -7856,6 +8250,10 @@ snapshots: dependencies: mime: 3.0.0 + '@cloudflare/kv-asset-handler@0.4.0': + dependencies: + mime: 3.0.0 + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -8251,6 +8649,10 @@ snapshots: '@fastify/busboy@3.1.1': {} + '@fingerprintjs/botd@1.9.1': + dependencies: + tslib: 2.8.1 + '@firebase/app-check-interop-types@0.3.3': {} '@firebase/app-types@0.9.3': {} @@ -8415,8 +8817,8 @@ snapshots: '@intlify/bundle-utils@10.0.0(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))': dependencies: - '@intlify/message-compiler': 11.0.0-rc.1 - '@intlify/shared': 11.0.0-rc.1 + '@intlify/message-compiler': 12.0.0-alpha.2 + '@intlify/shared': 12.0.0-alpha.2 acorn: 8.14.1 escodegen: 2.1.0 estree-walker: 2.0.2 @@ -8447,24 +8849,24 @@ snapshots: '@intlify/shared': 10.0.6 source-map-js: 1.2.1 - '@intlify/message-compiler@11.0.0-rc.1': + '@intlify/message-compiler@12.0.0-alpha.2': dependencies: - '@intlify/shared': 11.0.0-rc.1 + '@intlify/shared': 12.0.0-alpha.2 source-map-js: 1.2.1 '@intlify/shared@10.0.6': {} - '@intlify/shared@11.0.0-rc.1': {} - '@intlify/shared@11.1.2': {} - '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@3.29.5)(typescript@5.6.3)(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + '@intlify/shared@12.0.0-alpha.2': {} + + '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.6.3)(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': dependencies: '@eslint-community/eslint-utils': 4.5.0(eslint@9.22.0(jiti@2.4.2)) '@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3))) '@intlify/shared': 11.1.2 '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) '@typescript-eslint/scope-manager': 8.26.0 '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.6.3) debug: 4.4.0(supports-color@9.4.0) @@ -8613,11 +9015,11 @@ snapshots: - encoding - supports-color - '@miyaneee/rollup-plugin-json5@1.2.0(rollup@3.29.5)': + '@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.34.9)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) json5: 2.2.3 - rollup: 3.29.5 + rollup: 4.34.9 '@napi-rs/wasm-runtime@0.2.7': dependencies: @@ -8632,6 +9034,10 @@ snapshots: dependencies: '@netlify/serverless-functions-api': 1.30.1 + '@netlify/functions@3.0.4': + dependencies: + '@netlify/serverless-functions-api': 1.36.0 + '@netlify/node-cookies@0.1.0': {} '@netlify/serverless-functions-api@1.30.1': @@ -8639,6 +9045,8 @@ snapshots: '@netlify/node-cookies': 0.1.0 urlpattern-polyfill: 8.0.2 + '@netlify/serverless-functions-api@1.36.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8681,13 +9089,42 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/content@2.13.4(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + '@nuxt/cli@3.24.0(magicast@0.3.5)': + dependencies: + c12: 3.0.2(magicast@0.3.5) + chokidar: 4.0.3 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.4.2 + defu: 6.1.4 + fuse.js: 7.1.0 + giget: 2.0.0 + h3: 1.15.1 + httpxy: 0.1.7 + jiti: 2.4.2 + listhen: 1.9.0 + nypm: 0.6.0 + ofetch: 1.4.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + scule: 1.3.0 + semver: 7.7.1 + std-env: 3.8.1 + tinyexec: 1.0.1 + ufo: 1.5.4 + youch: 4.1.0-beta.6 + transitivePeerDependencies: + - magicast + + '@nuxt/content@2.13.4(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) '@nuxtjs/mdc': 0.9.5(magicast@0.3.5) '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) '@vueuse/head': 2.0.0(vue@3.5.13(typescript@5.6.3)) - '@vueuse/nuxt': 11.3.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@vueuse/nuxt': 11.3.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) consola: 3.4.0 defu: 6.1.4 destr: 2.0.3 @@ -8859,13 +9296,40 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/devtools-ui-kit@2.2.1(@nuxt/devtools@2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0)': + '@nuxt/devtools-kit@2.2.1(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + dependencies: + '@nuxt/kit': 3.16.0(magicast@0.3.5) + '@nuxt/schema': 3.16.0 + execa: 9.5.2 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - magicast + + '@nuxt/devtools-kit@2.3.2(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + dependencies: + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@nuxt/schema': 3.16.1 + execa: 8.0.1 + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - magicast + + '@nuxt/devtools-kit@2.3.2(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + dependencies: + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@nuxt/schema': 3.16.1 + execa: 8.0.1 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - magicast + + '@nuxt/devtools-ui-kit@2.2.1(@nuxt/devtools@2.3.2(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0)': dependencies: '@iconify-json/carbon': 1.2.8 '@iconify-json/logos': 1.2.4 '@iconify-json/ri': 1.2.5 '@iconify-json/tabler': 1.2.17 - '@nuxt/devtools': 2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@nuxt/devtools': 2.3.2(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) '@nuxt/devtools-kit': 2.2.1(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) '@nuxt/kit': 3.16.0(magicast@0.3.5) '@unocss/core': 66.0.0 @@ -8876,7 +9340,7 @@ snapshots: '@unocss/reset': 66.0.0 '@vueuse/core': 12.8.2(typescript@5.6.3) '@vueuse/integrations': 12.8.2(change-case@5.4.4)(focus-trap@7.6.4)(fuse.js@7.1.0)(typescript@5.6.3) - '@vueuse/nuxt': 12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3) + '@vueuse/nuxt': 12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3) defu: 6.1.4 focus-trap: 7.6.4 splitpanes: 3.2.0(vue@3.5.13(typescript@5.6.3)) @@ -8905,28 +9369,28 @@ snapshots: - vue - webpack - '@nuxt/devtools-ui-kit@2.2.1(@nuxt/devtools@2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0)': + '@nuxt/devtools-ui-kit@2.3.2(@nuxt/devtools@2.3.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)))(@unocss/webpack@66.0.0(webpack@5.98.0))(@vue/compiler-core@3.5.13)(change-case@5.4.4)(fuse.js@7.1.0)(magicast@0.3.5)(nuxt@3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0)': dependencies: '@iconify-json/carbon': 1.2.8 '@iconify-json/logos': 1.2.4 '@iconify-json/ri': 1.2.5 '@iconify-json/tabler': 1.2.17 - '@nuxt/devtools': 2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) - '@nuxt/devtools-kit': 2.2.1(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) - '@nuxt/kit': 3.16.0(magicast@0.3.5) + '@nuxt/devtools': 2.3.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@nuxt/kit': 3.16.1(magicast@0.3.5) '@unocss/core': 66.0.0 - '@unocss/nuxt': 66.0.0(magicast@0.3.5)(postcss@8.5.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) + '@unocss/nuxt': 66.0.0(magicast@0.3.5)(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0) '@unocss/preset-attributify': 66.0.0 '@unocss/preset-icons': 66.0.0 '@unocss/preset-mini': 66.0.0 '@unocss/reset': 66.0.0 - '@vueuse/core': 12.8.2(typescript@5.6.3) - '@vueuse/integrations': 12.8.2(change-case@5.4.4)(focus-trap@7.6.4)(fuse.js@7.1.0)(typescript@5.6.3) - '@vueuse/nuxt': 12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3) + '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.6.3)) + '@vueuse/integrations': 13.0.0(change-case@5.4.4)(focus-trap@7.6.4)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.6.3)) + '@vueuse/nuxt': 13.0.0(magicast@0.3.5)(nuxt@3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) defu: 6.1.4 focus-trap: 7.6.4 splitpanes: 3.2.0(vue@3.5.13(typescript@5.6.3)) - unocss: 66.0.0(@unocss/webpack@66.0.0(webpack@5.98.0))(postcss@8.5.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + unocss: 66.0.0(@unocss/webpack@66.0.0(webpack@5.98.0))(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) v-lazy-show: 0.3.0(@vue/compiler-core@3.5.13) transitivePeerDependencies: - '@unocss/webpack' @@ -8945,7 +9409,6 @@ snapshots: - qrcode - sortablejs - supports-color - - typescript - universal-cookie - vite - vue @@ -8962,6 +9425,17 @@ snapshots: prompts: 2.4.2 semver: 7.7.1 + '@nuxt/devtools-wizard@2.3.2': + dependencies: + consola: 3.4.2 + diff: 7.0.0 + execa: 8.0.1 + magicast: 0.3.5 + pathe: 2.0.3 + pkg-types: 2.1.0 + prompts: 2.4.2 + semver: 7.7.1 + '@nuxt/devtools@2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@nuxt/devtools-kit': 2.2.1(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) @@ -9003,6 +9477,129 @@ snapshots: - utf-8-validate - vue + '@nuxt/devtools@2.2.1(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@nuxt/devtools-kit': 2.2.1(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@nuxt/devtools-wizard': 2.2.1 + '@nuxt/kit': 3.16.0(magicast@0.3.5) + '@vue/devtools-core': 7.7.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.2 + birpc: 2.2.0 + consola: 3.4.0 + destr: 2.0.3 + error-stack-parser-es: 1.0.5 + execa: 9.5.2 + fast-npm-meta: 0.3.1 + get-port-please: 3.1.2 + hookable: 5.5.3 + image-meta: 0.2.1 + is-installed-globally: 1.0.0 + launch-editor: 2.10.0 + local-pkg: 1.1.1 + magicast: 0.3.5 + nypm: 0.6.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + semver: 7.7.1 + simple-git: 3.27.0 + sirv: 3.0.1 + structured-clone-es: 1.0.0 + tinyglobby: 0.2.12 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-plugin-inspect: 11.0.0(@nuxt/kit@3.16.0(magicast@0.3.5))(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vite-plugin-vue-tracer: 0.1.1(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + which: 5.0.0 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vue + + '@nuxt/devtools@2.3.2(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@nuxt/devtools-wizard': 2.3.2 + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@vue/devtools-core': 7.7.2(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.2 + birpc: 2.3.0 + consola: 3.4.2 + destr: 2.0.3 + error-stack-parser-es: 1.0.5 + execa: 8.0.1 + fast-npm-meta: 0.3.1 + get-port-please: 3.1.2 + hookable: 5.5.3 + image-meta: 0.2.1 + is-installed-globally: 1.0.0 + launch-editor: 2.10.0 + local-pkg: 1.1.1 + magicast: 0.3.5 + nypm: 0.6.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + semver: 7.7.1 + simple-git: 3.27.0 + sirv: 3.0.1 + structured-clone-es: 1.0.0 + tinyglobby: 0.2.12 + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-plugin-inspect: 11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vite-plugin-vue-tracer: 0.1.3(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + which: 5.0.0 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vue + + '@nuxt/devtools@2.3.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@nuxt/devtools-wizard': 2.3.2 + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@vue/devtools-core': 7.7.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.2 + birpc: 2.3.0 + consola: 3.4.2 + destr: 2.0.3 + error-stack-parser-es: 1.0.5 + execa: 8.0.1 + fast-npm-meta: 0.3.1 + get-port-please: 3.1.2 + hookable: 5.5.3 + image-meta: 0.2.1 + is-installed-globally: 1.0.0 + launch-editor: 2.10.0 + local-pkg: 1.1.1 + magicast: 0.3.5 + nypm: 0.6.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + semver: 7.7.1 + simple-git: 3.27.0 + sirv: 3.0.1 + structured-clone-es: 1.0.0 + tinyglobby: 0.2.12 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-plugin-inspect: 11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vite-plugin-vue-tracer: 0.1.3(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + which: 5.0.0 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vue + '@nuxt/kit@3.16.0(magicast@0.3.5)': dependencies: c12: 3.0.2(magicast@0.3.5) @@ -9030,6 +9627,33 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/kit@3.16.1(magicast@0.3.5)': + dependencies: + c12: 3.0.2(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.3 + errx: 0.1.0 + exsolve: 1.0.4 + globby: 14.1.0 + ignore: 7.0.3 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + mlly: 1.7.4 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.1.0 + scule: 1.3.0 + semver: 7.7.1 + std-env: 3.8.1 + ufo: 1.5.4 + unctx: 2.4.1 + unimport: 4.1.2 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + '@nuxt/module-builder@0.8.4(@nuxt/kit@3.16.0(magicast@0.3.5))(nuxi@3.22.5)(typescript@5.6.3)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) @@ -9056,6 +9680,13 @@ snapshots: pathe: 2.0.3 std-env: 3.8.1 + '@nuxt/schema@3.16.1': + dependencies: + consola: 3.4.2 + defu: 6.1.4 + pathe: 2.0.3 + std-env: 3.8.1 + '@nuxt/telemetry@2.6.5(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) @@ -9074,6 +9705,23 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/telemetry@2.6.6(magicast@0.3.5)': + dependencies: + '@nuxt/kit': 3.16.1(magicast@0.3.5) + citty: 0.1.6 + consola: 3.4.2 + destr: 2.0.3 + dotenv: 16.4.7 + git-url-parse: 16.0.1 + is-docker: 3.0.0 + ofetch: 1.4.1 + package-manager-detector: 1.1.0 + pathe: 2.0.3 + rc9: 2.1.2 + std-env: 3.8.1 + transitivePeerDependencies: + - magicast + '@nuxt/test-utils@3.17.2(@types/node@22.13.10)(jiti@2.4.2)(magicast@0.3.5)(terser@5.39.0)(typescript@5.6.3)(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) @@ -9119,10 +9767,10 @@ snapshots: - typescript - yaml - '@nuxt/vite-builder@3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': + '@nuxt/vite-builder@3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) - '@rollup/plugin-replace': 6.0.2(rollup@3.29.5) + '@rollup/plugin-replace': 6.0.2(rollup@4.34.9) '@vitejs/plugin-vue': 5.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) '@vitejs/plugin-vue-jsx': 4.1.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) autoprefixer: 10.4.20(postcss@8.5.3) @@ -9144,7 +9792,7 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 2.1.0 postcss: 8.5.3 - rollup-plugin-visualizer: 5.14.0(rollup@3.29.5) + rollup-plugin-visualizer: 5.14.0(rollup@4.34.9) std-env: 3.8.1 ufo: 1.5.4 unenv: 2.0.0-rc.12 @@ -9179,10 +9827,10 @@ snapshots: - vue-tsc - yaml - '@nuxt/vite-builder@3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': + '@nuxt/vite-builder@3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) - '@rollup/plugin-replace': 6.0.2(rollup@4.34.9) + '@rollup/plugin-replace': 6.0.2(rollup@4.38.0) '@vitejs/plugin-vue': 5.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) '@vitejs/plugin-vue-jsx': 4.1.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) autoprefixer: 10.4.20(postcss@8.5.3) @@ -9204,7 +9852,7 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 2.1.0 postcss: 8.5.3 - rollup-plugin-visualizer: 5.14.0(rollup@4.34.9) + rollup-plugin-visualizer: 5.14.0(rollup@4.38.0) std-env: 3.8.1 ufo: 1.5.4 unenv: 2.0.0-rc.12 @@ -9239,19 +9887,19 @@ snapshots: - vue-tsc - yaml - '@nuxt/vite-builder@3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.35.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': + '@nuxt/vite-builder@3.16.1(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0)': dependencies: - '@nuxt/kit': 3.16.0(magicast@0.3.5) - '@rollup/plugin-replace': 6.0.2(rollup@4.35.0) - '@vitejs/plugin-vue': 5.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) - '@vitejs/plugin-vue-jsx': 4.1.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) - autoprefixer: 10.4.20(postcss@8.5.3) - consola: 3.4.0 + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@rollup/plugin-replace': 6.0.2(rollup@4.38.0) + '@vitejs/plugin-vue': 5.2.3(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@vitejs/plugin-vue-jsx': 4.1.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + autoprefixer: 10.4.21(postcss@8.5.3) + consola: 3.4.2 cssnano: 7.0.6(postcss@8.5.3) defu: 6.1.4 - esbuild: 0.25.0 + esbuild: 0.25.1 escape-string-regexp: 5.0.0 - exsolve: 1.0.2 + exsolve: 1.0.4 externality: 1.0.2 get-port-please: 3.1.2 h3: 1.15.1 @@ -9259,19 +9907,20 @@ snapshots: knitwork: 1.2.0 magic-string: 0.30.17 mlly: 1.7.4 + mocked-exports: 0.1.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 1.0.0 pkg-types: 2.1.0 postcss: 8.5.3 - rollup-plugin-visualizer: 5.14.0(rollup@4.35.0) + rollup-plugin-visualizer: 5.14.0(rollup@4.38.0) std-env: 3.8.1 ufo: 1.5.4 - unenv: 2.0.0-rc.12 - unplugin: 2.2.0 - vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) - vite-node: 3.0.8(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) - vite-plugin-checker: 0.9.0(eslint@9.22.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + unenv: 2.0.0-rc.15 + unplugin: 2.2.2 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-node: 3.1.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-plugin-checker: 0.9.1(eslint@9.22.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) vue: 3.5.13(typescript@5.6.3) vue-bundle-renderer: 2.1.1 transitivePeerDependencies: @@ -9310,11 +9959,11 @@ snapshots: '@nuxtjs/eslint-config-typescript@12.1.0(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)': dependencies: - '@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) + '@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@typescript-eslint/parser': 6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) eslint: 9.22.0(jiti@2.4.2) - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)) eslint-plugin-vue: 9.33.0(eslint@9.22.0(jiti@2.4.2)) transitivePeerDependencies: @@ -9323,10 +9972,10 @@ snapshots: - supports-color - typescript - '@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))': + '@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2))': dependencies: eslint: 9.22.0(jiti@2.4.2) - eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-n@15.7.0(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-promise@6.6.0(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-promise@6.6.0(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)) eslint-plugin-n: 15.7.0(eslint@9.22.0(jiti@2.4.2)) eslint-plugin-node: 11.1.0(eslint@9.22.0(jiti@2.4.2)) @@ -9340,16 +9989,16 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@nuxtjs/i18n@9.3.1(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(rollup@3.29.5)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))': + '@nuxtjs/i18n@9.3.1(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(rollup@4.34.9)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))': dependencies: '@intlify/h3': 0.6.1 '@intlify/shared': 10.0.6 - '@intlify/unplugin-vue-i18n': 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@3.29.5)(typescript@5.6.3)(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) + '@intlify/unplugin-vue-i18n': 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.22.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.6.3)(vue-i18n@10.0.6(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) '@intlify/utils': 0.13.0 - '@miyaneee/rollup-plugin-json5': 1.2.0(rollup@3.29.5) + '@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.34.9) '@nuxt/kit': 3.16.0(magicast@0.3.5) '@oxc-parser/wasm': 0.53.0 - '@rollup/plugin-yaml': 4.1.2(rollup@3.29.5) + '@rollup/plugin-yaml': 4.1.2(rollup@4.34.9) '@vue/compiler-sfc': 3.5.13 debug: 4.4.0(supports-color@9.4.0) defu: 6.1.4 @@ -9362,7 +10011,7 @@ snapshots: pathe: 1.1.2 ufo: 1.5.4 unplugin: 2.2.0 - unplugin-vue-router: 0.10.9(rollup@3.29.5)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) + unplugin-vue-router: 0.10.9(rollup@4.34.9)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) vue-i18n: 10.0.6(vue@3.5.13(typescript@5.6.3)) vue-router: 4.5.0(vue@3.5.13(typescript@5.6.3)) transitivePeerDependencies: @@ -9578,10 +10227,16 @@ snapshots: dependencies: '@oxc-project/types': 0.56.4 + '@oxc-parser/wasm@0.60.0': + dependencies: + '@oxc-project/types': 0.60.0 + '@oxc-project/types@0.53.0': {} '@oxc-project/types@0.56.4': {} + '@oxc-project/types@0.60.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -9720,6 +10375,10 @@ snapshots: optionalDependencies: rollup: 4.34.9 + '@rollup/plugin-alias@5.1.1(rollup@4.38.0)': + optionalDependencies: + rollup: 4.38.0 + '@rollup/plugin-commonjs@25.0.8(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@3.29.5) @@ -9743,6 +10402,18 @@ snapshots: optionalDependencies: rollup: 4.34.9 + '@rollup/plugin-commonjs@28.0.3(rollup@4.38.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.3(picomatch@4.0.2) + is-reference: 1.2.1 + magic-string: 0.30.17 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.38.0 + '@rollup/plugin-inject@5.0.5(rollup@4.34.9)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.34.9) @@ -9751,6 +10422,14 @@ snapshots: optionalDependencies: rollup: 4.34.9 + '@rollup/plugin-inject@5.0.5(rollup@4.38.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) + estree-walker: 2.0.2 + magic-string: 0.30.17 + optionalDependencies: + rollup: 4.38.0 + '@rollup/plugin-json@6.1.0(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@3.29.5) @@ -9763,6 +10442,12 @@ snapshots: optionalDependencies: rollup: 4.34.9 + '@rollup/plugin-json@6.1.0(rollup@4.38.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) + optionalDependencies: + rollup: 4.38.0 + '@rollup/plugin-node-resolve@15.3.1(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@3.29.5) @@ -9775,13 +10460,23 @@ snapshots: '@rollup/plugin-node-resolve@16.0.0(rollup@4.34.9)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.34.9) + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 4.34.9 + + '@rollup/plugin-node-resolve@16.0.1(rollup@4.38.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.34.9 + rollup: 4.38.0 '@rollup/plugin-replace@5.0.7(rollup@3.29.5)': dependencies: @@ -9790,13 +10485,6 @@ snapshots: optionalDependencies: rollup: 3.29.5 - '@rollup/plugin-replace@6.0.2(rollup@3.29.5)': - dependencies: - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) - magic-string: 0.30.17 - optionalDependencies: - rollup: 3.29.5 - '@rollup/plugin-replace@6.0.2(rollup@4.34.9)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.34.9) @@ -9804,12 +10492,12 @@ snapshots: optionalDependencies: rollup: 4.34.9 - '@rollup/plugin-replace@6.0.2(rollup@4.35.0)': + '@rollup/plugin-replace@6.0.2(rollup@4.38.0)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) magic-string: 0.30.17 optionalDependencies: - rollup: 4.35.0 + rollup: 4.38.0 '@rollup/plugin-terser@0.4.4(rollup@4.34.9)': dependencies: @@ -9819,13 +10507,21 @@ snapshots: optionalDependencies: rollup: 4.34.9 - '@rollup/plugin-yaml@4.1.2(rollup@3.29.5)': + '@rollup/plugin-terser@0.4.4(rollup@4.38.0)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.39.0 + optionalDependencies: + rollup: 4.38.0 + + '@rollup/plugin-yaml@4.1.2(rollup@4.34.9)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) js-yaml: 4.1.0 tosource: 2.0.0-alpha.3 optionalDependencies: - rollup: 3.29.5 + rollup: 4.34.9 '@rollup/pluginutils@5.1.4(rollup@3.29.5)': dependencies: @@ -9843,13 +10539,13 @@ snapshots: optionalDependencies: rollup: 4.34.9 - '@rollup/pluginutils@5.1.4(rollup@4.35.0)': + '@rollup/pluginutils@5.1.4(rollup@4.38.0)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.35.0 + rollup: 4.38.0 '@rollup/rollup-android-arm-eabi@4.34.9': optional: true @@ -9857,114 +10553,174 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.35.0': optional: true + '@rollup/rollup-android-arm-eabi@4.38.0': + optional: true + '@rollup/rollup-android-arm64@4.34.9': optional: true '@rollup/rollup-android-arm64@4.35.0': optional: true + '@rollup/rollup-android-arm64@4.38.0': + optional: true + '@rollup/rollup-darwin-arm64@4.34.9': optional: true '@rollup/rollup-darwin-arm64@4.35.0': optional: true + '@rollup/rollup-darwin-arm64@4.38.0': + optional: true + '@rollup/rollup-darwin-x64@4.34.9': optional: true '@rollup/rollup-darwin-x64@4.35.0': optional: true + '@rollup/rollup-darwin-x64@4.38.0': + optional: true + '@rollup/rollup-freebsd-arm64@4.34.9': optional: true '@rollup/rollup-freebsd-arm64@4.35.0': optional: true + '@rollup/rollup-freebsd-arm64@4.38.0': + optional: true + '@rollup/rollup-freebsd-x64@4.34.9': optional: true '@rollup/rollup-freebsd-x64@4.35.0': optional: true + '@rollup/rollup-freebsd-x64@4.38.0': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.35.0': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.38.0': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.34.9': optional: true '@rollup/rollup-linux-arm-musleabihf@4.35.0': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.38.0': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.34.9': optional: true '@rollup/rollup-linux-arm64-gnu@4.35.0': optional: true + '@rollup/rollup-linux-arm64-gnu@4.38.0': + optional: true + '@rollup/rollup-linux-arm64-musl@4.34.9': optional: true '@rollup/rollup-linux-arm64-musl@4.35.0': optional: true + '@rollup/rollup-linux-arm64-musl@4.38.0': + optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': optional: true '@rollup/rollup-linux-loongarch64-gnu@4.35.0': optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.38.0': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': optional: true '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.38.0': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.34.9': optional: true '@rollup/rollup-linux-riscv64-gnu@4.35.0': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.38.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.38.0': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.34.9': optional: true '@rollup/rollup-linux-s390x-gnu@4.35.0': optional: true + '@rollup/rollup-linux-s390x-gnu@4.38.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.34.9': optional: true '@rollup/rollup-linux-x64-gnu@4.35.0': optional: true + '@rollup/rollup-linux-x64-gnu@4.38.0': + optional: true + '@rollup/rollup-linux-x64-musl@4.34.9': optional: true '@rollup/rollup-linux-x64-musl@4.35.0': optional: true + '@rollup/rollup-linux-x64-musl@4.38.0': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.34.9': optional: true '@rollup/rollup-win32-arm64-msvc@4.35.0': optional: true + '@rollup/rollup-win32-arm64-msvc@4.38.0': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.34.9': optional: true '@rollup/rollup-win32-ia32-msvc@4.35.0': optional: true + '@rollup/rollup-win32-ia32-msvc@4.38.0': + optional: true + '@rollup/rollup-win32-x64-msvc@4.34.9': optional: true '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true + '@rollup/rollup-win32-x64-msvc@4.38.0': + optional: true + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -10140,6 +10896,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/estree@1.0.7': {} + '@types/express-serve-static-core@4.19.6': dependencies: '@types/node': 22.13.9 @@ -10258,10 +11016,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/type-utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) @@ -10463,6 +11221,12 @@ snapshots: unhead: 2.0.0-rc.7 vue: 3.5.13(typescript@5.6.3) + '@unhead/vue@2.0.2(vue@3.5.13(typescript@5.6.3))': + dependencies: + hookable: 5.5.3 + unhead: 2.0.2 + vue: 3.5.13(typescript@5.6.3) + '@unocss/astro@66.0.0(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@unocss/core': 66.0.0 @@ -10473,6 +11237,16 @@ snapshots: transitivePeerDependencies: - vue + '@unocss/astro@66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@unocss/core': 66.0.0 + '@unocss/reset': 66.0.0 + '@unocss/vite': 66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + optionalDependencies: + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - vue + '@unocss/cli@66.0.0': dependencies: '@ampproject/remapping': 2.3.0 @@ -10535,6 +11309,30 @@ snapshots: - vue - webpack + '@unocss/nuxt@66.0.0(magicast@0.3.5)(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))(webpack@5.98.0)': + dependencies: + '@nuxt/kit': 3.16.0(magicast@0.3.5) + '@unocss/config': 66.0.0 + '@unocss/core': 66.0.0 + '@unocss/preset-attributify': 66.0.0 + '@unocss/preset-icons': 66.0.0 + '@unocss/preset-tagify': 66.0.0 + '@unocss/preset-typography': 66.0.0 + '@unocss/preset-uno': 66.0.0 + '@unocss/preset-web-fonts': 66.0.0 + '@unocss/preset-wind': 66.0.0 + '@unocss/reset': 66.0.0 + '@unocss/vite': 66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@unocss/webpack': 66.0.0(webpack@5.98.0) + unocss: 66.0.0(@unocss/webpack@66.0.0(webpack@5.98.0))(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + transitivePeerDependencies: + - magicast + - postcss + - supports-color + - vite + - vue + - webpack + '@unocss/postcss@66.0.0(postcss@8.5.3)': dependencies: '@unocss/config': 66.0.0 @@ -10638,6 +11436,20 @@ snapshots: transitivePeerDependencies: - vue + '@unocss/vite@66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@unocss/config': 66.0.0 + '@unocss/core': 66.0.0 + '@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.6.3)) + chokidar: 3.6.0 + magic-string: 0.30.17 + tinyglobby: 0.2.12 + unplugin-utils: 0.2.4 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - vue + '@unocss/webpack@66.0.0(webpack@5.98.0)': dependencies: '@ampproject/remapping': 2.3.0 @@ -10670,6 +11482,25 @@ snapshots: - rollup - supports-color + '@vercel/nft@0.29.2(rollup@4.38.0)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.0 + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + '@vitejs/plugin-vue-jsx@4.1.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@babel/core': 7.26.9 @@ -10680,11 +11511,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue-jsx@4.1.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@babel/core': 7.26.9 + '@babel/plugin-transform-typescript': 7.26.8(@babel/core@7.26.9) + '@vue/babel-plugin-jsx': 1.3.0(@babel/core@7.26.9) + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-vue@5.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vue: 3.5.13(typescript@5.6.3) + '@vitejs/plugin-vue@5.2.3(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.3) + '@vitest/eslint-plugin@1.1.37(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3)(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': dependencies: '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) @@ -10834,6 +11680,18 @@ snapshots: transitivePeerDependencies: - vite + '@vue/devtools-core@7.7.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@vue/devtools-kit': 7.7.2 + '@vue/devtools-shared': 7.7.2 + mitt: 3.0.1 + nanoid: 5.1.3 + pathe: 2.0.3 + vite-hot-client: 0.2.4(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vue: 3.5.13(typescript@5.6.3) + transitivePeerDependencies: + - vite + '@vue/devtools-kit@7.7.2': dependencies: '@vue/devtools-shared': 7.7.2 @@ -10931,56 +11789,65 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/integrations@13.0.0(change-case@5.4.4)(focus-trap@7.6.4)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.6.3)) + '@vueuse/shared': 13.0.0(vue@3.5.13(typescript@5.6.3)) + vue: 3.5.13(typescript@5.6.3) + optionalDependencies: + change-case: 5.4.4 + focus-trap: 7.6.4 + fuse.js: 7.1.0 + '@vueuse/metadata@11.3.0': {} '@vueuse/metadata@12.8.2': {} '@vueuse/metadata@13.0.0': {} - '@vueuse/nuxt@11.3.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + '@vueuse/nuxt@11.3.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) '@vueuse/metadata': 11.3.0 local-pkg: 0.5.1 - nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) transitivePeerDependencies: - '@vue/composition-api' - magicast - vue - '@vueuse/nuxt@12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3)': + '@vueuse/nuxt@12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3)': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) '@vueuse/core': 12.8.2(typescript@5.6.3) '@vueuse/metadata': 12.8.2 local-pkg: 1.1.1 - nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) vue: 3.5.13(typescript@5.6.3) transitivePeerDependencies: - magicast - typescript - '@vueuse/nuxt@12.8.2(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(typescript@5.6.3)': + '@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) - '@vueuse/core': 12.8.2(typescript@5.6.3) - '@vueuse/metadata': 12.8.2 + '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.6.3)) + '@vueuse/metadata': 13.0.0 local-pkg: 1.1.1 nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) vue: 3.5.13(typescript@5.6.3) transitivePeerDependencies: - magicast - - typescript - '@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': + '@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3))': dependencies: '@nuxt/kit': 3.16.0(magicast@0.3.5) '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.6.3)) '@vueuse/metadata': 13.0.0 local-pkg: 1.1.1 - nuxt: 3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) + nuxt: 3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) vue: 3.5.13(typescript@5.6.3) transitivePeerDependencies: - magicast @@ -11274,6 +12141,16 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001702 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -11306,6 +12183,8 @@ snapshots: birpc@2.2.0: {} + birpc@2.3.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -11587,6 +12466,8 @@ snapshots: consola@3.4.0: {} + consola@3.4.2: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -12170,7 +13051,7 @@ snapshots: '@eslint/compat': 1.2.7(eslint@9.22.0(jiti@2.4.2)) eslint: 9.22.0(jiti@2.4.2) - eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-n@15.7.0(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-promise@6.6.0(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)): + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@9.22.0(jiti@2.4.2)))(eslint-plugin-promise@6.6.0(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)): dependencies: eslint: 9.22.0(jiti@2.4.2) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)) @@ -12189,7 +13070,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@9.4.0) @@ -12215,14 +13096,14 @@ snapshots: dependencies: eslint: 9.22.0(jiti@2.4.2) - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) eslint: 9.22.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -12285,7 +13166,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.22.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.22.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12457,7 +13338,7 @@ snapshots: dependencies: eslint: 9.22.0(jiti@2.4.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@6.21.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.6.3) eslint-plugin-vue@10.0.0(eslint@9.22.0(jiti@2.4.2))(vue-eslint-parser@10.1.1(eslint@9.22.0(jiti@2.4.2))): dependencies: @@ -13368,9 +14249,9 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - impound@0.2.0(rollup@3.29.5): + impound@0.2.0(rollup@4.34.9): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) mlly: 1.7.4 pathe: 1.1.2 unenv: 1.10.0 @@ -13378,9 +14259,9 @@ snapshots: transitivePeerDependencies: - rollup - impound@0.2.0(rollup@4.34.9): + impound@0.2.0(rollup@4.38.0): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.34.9) + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) mlly: 1.7.4 pathe: 1.1.2 unenv: 1.10.0 @@ -13388,13 +14269,13 @@ snapshots: transitivePeerDependencies: - rollup - impound@0.2.0(rollup@4.35.0): + impound@0.2.2(rollup@4.38.0): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + '@rollup/pluginutils': 5.1.4(rollup@4.38.0) mlly: 1.7.4 - pathe: 1.1.2 - unenv: 1.10.0 - unplugin: 1.16.1 + mocked-exports: 0.1.1 + pathe: 2.0.3 + unplugin: 2.2.2 transitivePeerDependencies: - rollup @@ -14347,6 +15228,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.5.4 + mocked-exports@0.1.1: {} + mrmime@2.0.1: {} ms@2.0.0: {} @@ -14371,26 +15254,129 @@ snapshots: neo-async@2.6.2: {} - nitropack@2.11.5(@libsql/client@0.14.0)(better-sqlite3@11.8.1)(typescript@5.6.3): - dependencies: - '@cloudflare/kv-asset-handler': 0.3.4 - '@netlify/functions': 3.0.0 - '@rollup/plugin-alias': 5.1.1(rollup@4.34.9) - '@rollup/plugin-commonjs': 28.0.3(rollup@4.34.9) - '@rollup/plugin-inject': 5.0.5(rollup@4.34.9) - '@rollup/plugin-json': 6.1.0(rollup@4.34.9) - '@rollup/plugin-node-resolve': 16.0.0(rollup@4.34.9) - '@rollup/plugin-replace': 6.0.2(rollup@4.34.9) - '@rollup/plugin-terser': 0.4.4(rollup@4.34.9) - '@types/http-proxy': 1.17.16 - '@vercel/nft': 0.29.2(rollup@4.34.9) + nitropack@2.11.5(@libsql/client@0.14.0)(better-sqlite3@11.8.1)(typescript@5.6.3): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@netlify/functions': 3.0.0 + '@rollup/plugin-alias': 5.1.1(rollup@4.34.9) + '@rollup/plugin-commonjs': 28.0.3(rollup@4.34.9) + '@rollup/plugin-inject': 5.0.5(rollup@4.34.9) + '@rollup/plugin-json': 6.1.0(rollup@4.34.9) + '@rollup/plugin-node-resolve': 16.0.0(rollup@4.34.9) + '@rollup/plugin-replace': 6.0.2(rollup@4.34.9) + '@rollup/plugin-terser': 0.4.4(rollup@4.34.9) + '@types/http-proxy': 1.17.16 + '@vercel/nft': 0.29.2(rollup@4.34.9) + archiver: 7.0.1 + c12: 3.0.2(magicast@0.3.5) + chokidar: 4.0.3 + citty: 0.1.6 + compatx: 0.1.8 + confbox: 0.2.1 + consola: 3.4.0 + cookie-es: 2.0.0 + croner: 9.0.0 + crossws: 0.3.4 + db0: 0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1) + defu: 6.1.4 + destr: 2.0.3 + dot-prop: 9.0.0 + esbuild: 0.25.0 + escape-string-regexp: 5.0.0 + etag: 1.8.1 + exsolve: 1.0.2 + fs-extra: 11.3.0 + globby: 14.1.0 + gzip-size: 7.0.0 + h3: 1.15.1 + hookable: 5.5.3 + httpxy: 0.1.7 + ioredis: 5.6.0 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + listhen: 1.9.0 + magic-string: 0.30.17 + magicast: 0.3.5 + mime: 4.0.6 + mlly: 1.7.4 + node-fetch-native: 1.6.6 + node-mock-http: 1.0.0 + ofetch: 1.4.1 + ohash: 2.0.11 + openapi-typescript: 7.6.1(typescript@5.6.3) + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + pretty-bytes: 6.1.1 + radix3: 1.1.2 + rollup: 4.34.9 + rollup-plugin-visualizer: 5.14.0(rollup@4.34.9) + scule: 1.3.0 + semver: 7.7.1 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + source-map: 0.7.4 + std-env: 3.8.1 + ufo: 1.5.4 + ultrahtml: 1.5.3 + uncrypto: 0.1.3 + unctx: 2.4.1 + unenv: 2.0.0-rc.12 + unimport: 4.1.2 + unplugin-utils: 0.2.4 + unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0) + untyped: 2.0.0 + unwasm: 0.3.9 + youch: 4.1.0-beta.6 + youch-core: 0.3.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - rolldown + - sqlite3 + - supports-color + - typescript + - uploadthing + + nitropack@2.11.8(@libsql/client@0.14.0)(better-sqlite3@11.8.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.0 + '@netlify/functions': 3.0.4 + '@rollup/plugin-alias': 5.1.1(rollup@4.38.0) + '@rollup/plugin-commonjs': 28.0.3(rollup@4.38.0) + '@rollup/plugin-inject': 5.0.5(rollup@4.38.0) + '@rollup/plugin-json': 6.1.0(rollup@4.38.0) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.38.0) + '@rollup/plugin-replace': 6.0.2(rollup@4.38.0) + '@rollup/plugin-terser': 0.4.4(rollup@4.38.0) + '@vercel/nft': 0.29.2(rollup@4.38.0) archiver: 7.0.1 c12: 3.0.2(magicast@0.3.5) chokidar: 4.0.3 citty: 0.1.6 compatx: 0.1.8 confbox: 0.2.1 - consola: 3.4.0 + consola: 3.4.2 cookie-es: 2.0.0 croner: 9.0.0 crossws: 0.3.4 @@ -14398,11 +15384,10 @@ snapshots: defu: 6.1.4 destr: 2.0.3 dot-prop: 9.0.0 - esbuild: 0.25.0 + esbuild: 0.25.1 escape-string-regexp: 5.0.0 etag: 1.8.1 - exsolve: 1.0.2 - fs-extra: 11.3.0 + exsolve: 1.0.4 globby: 14.1.0 gzip-size: 7.0.0 h3: 1.15.1 @@ -14421,14 +15406,13 @@ snapshots: node-mock-http: 1.0.0 ofetch: 1.4.1 ohash: 2.0.11 - openapi-typescript: 7.6.1(typescript@5.6.3) pathe: 2.0.3 perfect-debounce: 1.0.0 pkg-types: 2.1.0 pretty-bytes: 6.1.1 radix3: 1.1.2 - rollup: 4.34.9 - rollup-plugin-visualizer: 5.14.0(rollup@4.34.9) + rollup: 4.38.0 + rollup-plugin-visualizer: 5.14.0(rollup@4.38.0) scule: 1.3.0 semver: 7.7.1 serve-placeholder: 2.0.2 @@ -14439,14 +15423,14 @@ snapshots: ultrahtml: 1.5.3 uncrypto: 0.1.3 unctx: 2.4.1 - unenv: 2.0.0-rc.12 + unenv: 2.0.0-rc.15 unimport: 4.1.2 unplugin-utils: 0.2.4 unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0) untyped: 2.0.0 unwasm: 0.3.9 youch: 4.1.0-beta.6 - youch-core: 0.3.1 + youch-core: 0.3.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -14472,7 +15456,6 @@ snapshots: - rolldown - sqlite3 - supports-color - - typescript - uploadthing node-abi@3.74.0: @@ -14582,7 +15565,7 @@ snapshots: - magicast - vue - nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): + nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): dependencies: '@nuxt/cli': 3.22.5(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -14590,7 +15573,7 @@ snapshots: '@nuxt/kit': 3.16.0(magicast@0.3.5) '@nuxt/schema': 3.16.0 '@nuxt/telemetry': 2.6.5(magicast@0.3.5) - '@nuxt/vite-builder': 3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) + '@nuxt/vite-builder': 3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) '@oxc-parser/wasm': 0.56.4 '@unhead/vue': 2.0.0-rc.7(vue@3.5.13(typescript@5.6.3)) '@vue/shared': 3.5.13 @@ -14611,7 +15594,7 @@ snapshots: h3: 1.15.1 hookable: 5.5.3 ignore: 7.0.3 - impound: 0.2.0(rollup@3.29.5) + impound: 0.2.0(rollup@4.34.9) jiti: 2.4.2 klona: 2.0.6 knitwork: 1.2.0 @@ -14703,15 +15686,15 @@ snapshots: - xml2js - yaml - nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): + nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): dependencies: '@nuxt/cli': 3.22.5(magicast@0.3.5) '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@nuxt/devtools': 2.2.1(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) '@nuxt/kit': 3.16.0(magicast@0.3.5) '@nuxt/schema': 3.16.0 '@nuxt/telemetry': 2.6.5(magicast@0.3.5) - '@nuxt/vite-builder': 3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.9)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) + '@nuxt/vite-builder': 3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) '@oxc-parser/wasm': 0.56.4 '@unhead/vue': 2.0.0-rc.7(vue@3.5.13(typescript@5.6.3)) '@vue/shared': 3.5.13 @@ -14732,7 +15715,7 @@ snapshots: h3: 1.15.1 hookable: 5.5.3 ignore: 7.0.3 - impound: 0.2.0(rollup@4.34.9) + impound: 0.2.0(rollup@4.38.0) jiti: 2.4.2 klona: 2.0.6 knitwork: 1.2.0 @@ -14824,43 +15807,44 @@ snapshots: - xml2js - yaml - nuxt@3.16.0(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.35.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): + nuxt@3.16.1(@libsql/client@0.14.0)(@parcel/watcher@2.5.1)(@types/node@22.13.10)(better-sqlite3@11.8.1)(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(eslint@9.22.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): dependencies: - '@nuxt/cli': 3.22.5(magicast@0.3.5) + '@nuxt/cli': 3.24.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 2.2.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) - '@nuxt/kit': 3.16.0(magicast@0.3.5) - '@nuxt/schema': 3.16.0 - '@nuxt/telemetry': 2.6.5(magicast@0.3.5) - '@nuxt/vite-builder': 3.16.0(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.35.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) - '@oxc-parser/wasm': 0.56.4 - '@unhead/vue': 2.0.0-rc.7(vue@3.5.13(typescript@5.6.3)) + '@nuxt/devtools': 2.3.2(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@nuxt/kit': 3.16.1(magicast@0.3.5) + '@nuxt/schema': 3.16.1 + '@nuxt/telemetry': 2.6.6(magicast@0.3.5) + '@nuxt/vite-builder': 3.16.1(@types/node@22.13.10)(eslint@9.22.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.38.0)(terser@5.39.0)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))(yaml@2.7.0) + '@oxc-parser/wasm': 0.60.0 + '@unhead/vue': 2.0.2(vue@3.5.13(typescript@5.6.3)) '@vue/shared': 3.5.13 c12: 3.0.2(magicast@0.3.5) chokidar: 4.0.3 compatx: 0.1.8 - consola: 3.4.0 + consola: 3.4.2 cookie-es: 2.0.0 defu: 6.1.4 destr: 2.0.3 devalue: 5.1.1 errx: 0.1.0 - esbuild: 0.25.0 + esbuild: 0.25.1 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 - exsolve: 1.0.2 + exsolve: 1.0.4 globby: 14.1.0 h3: 1.15.1 hookable: 5.5.3 ignore: 7.0.3 - impound: 0.2.0(rollup@4.35.0) + impound: 0.2.2(rollup@4.38.0) jiti: 2.4.2 klona: 2.0.6 knitwork: 1.2.0 magic-string: 0.30.17 mlly: 1.7.4 + mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.11.5(@libsql/client@0.14.0)(better-sqlite3@11.8.1)(typescript@5.6.3) + nitropack: 2.11.8(@libsql/client@0.14.0)(better-sqlite3@11.8.1) nypm: 0.6.0 ofetch: 1.4.1 ohash: 2.0.11 @@ -14879,9 +15863,8 @@ snapshots: ultrahtml: 1.5.3 uncrypto: 0.1.3 unctx: 2.4.1 - unenv: 2.0.0-rc.12 unimport: 4.1.2 - unplugin: 2.2.0 + unplugin: 2.2.2 unplugin-vue-router: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0) untyped: 2.0.0 @@ -15131,6 +16114,8 @@ snapshots: package-manager-detector@1.0.0: {} + package-manager-detector@1.1.0: {} + packrup@0.1.2: {} parent-module@1.0.1: @@ -15848,15 +16833,6 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.26.2 - rollup-plugin-visualizer@5.14.0(rollup@3.29.5): - dependencies: - open: 8.4.2 - picomatch: 4.0.2 - source-map: 0.7.4 - yargs: 17.7.2 - optionalDependencies: - rollup: 3.29.5 - rollup-plugin-visualizer@5.14.0(rollup@4.34.9): dependencies: open: 8.4.2 @@ -15866,14 +16842,14 @@ snapshots: optionalDependencies: rollup: 4.34.9 - rollup-plugin-visualizer@5.14.0(rollup@4.35.0): + rollup-plugin-visualizer@5.14.0(rollup@4.38.0): dependencies: open: 8.4.2 picomatch: 4.0.2 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.35.0 + rollup: 4.38.0 rollup@3.29.5: optionalDependencies: @@ -15929,6 +16905,32 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.35.0 fsevents: 2.3.3 + rollup@4.38.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.38.0 + '@rollup/rollup-android-arm64': 4.38.0 + '@rollup/rollup-darwin-arm64': 4.38.0 + '@rollup/rollup-darwin-x64': 4.38.0 + '@rollup/rollup-freebsd-arm64': 4.38.0 + '@rollup/rollup-freebsd-x64': 4.38.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.38.0 + '@rollup/rollup-linux-arm-musleabihf': 4.38.0 + '@rollup/rollup-linux-arm64-gnu': 4.38.0 + '@rollup/rollup-linux-arm64-musl': 4.38.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.38.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.38.0 + '@rollup/rollup-linux-riscv64-gnu': 4.38.0 + '@rollup/rollup-linux-riscv64-musl': 4.38.0 + '@rollup/rollup-linux-s390x-gnu': 4.38.0 + '@rollup/rollup-linux-x64-gnu': 4.38.0 + '@rollup/rollup-linux-x64-musl': 4.38.0 + '@rollup/rollup-win32-arm64-msvc': 4.38.0 + '@rollup/rollup-win32-ia32-msvc': 4.38.0 + '@rollup/rollup-win32-x64-msvc': 4.38.0 + fsevents: 2.3.3 + run-applescript@7.0.0: {} run-parallel@1.2.0: @@ -16446,6 +17448,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -16635,6 +17639,14 @@ snapshots: pathe: 2.0.3 ufo: 1.5.4 + unenv@2.0.0-rc.15: + dependencies: + defu: 6.1.4 + exsolve: 1.0.4 + ohash: 2.0.11 + pathe: 2.0.3 + ufo: 1.5.4 + unhead@1.11.20: dependencies: '@unhead/dom': 1.11.20 @@ -16646,6 +17658,10 @@ snapshots: dependencies: hookable: 5.5.3 + unhead@2.0.2: + dependencies: + hookable: 5.5.3 + unicode-emoji-modifier-base@1.0.0: {} unicorn-magic@0.1.0: {} @@ -16741,6 +17757,34 @@ snapshots: - supports-color - vue + unocss@66.0.0(@unocss/webpack@66.0.0(webpack@5.98.0))(postcss@8.5.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)): + dependencies: + '@unocss/astro': 66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + '@unocss/cli': 66.0.0 + '@unocss/core': 66.0.0 + '@unocss/postcss': 66.0.0(postcss@8.5.3) + '@unocss/preset-attributify': 66.0.0 + '@unocss/preset-icons': 66.0.0 + '@unocss/preset-mini': 66.0.0 + '@unocss/preset-tagify': 66.0.0 + '@unocss/preset-typography': 66.0.0 + '@unocss/preset-uno': 66.0.0 + '@unocss/preset-web-fonts': 66.0.0 + '@unocss/preset-wind': 66.0.0 + '@unocss/preset-wind3': 66.0.0 + '@unocss/transformer-attributify-jsx': 66.0.0 + '@unocss/transformer-compile-class': 66.0.0 + '@unocss/transformer-directives': 66.0.0 + '@unocss/transformer-variant-group': 66.0.0 + '@unocss/vite': 66.0.0(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)) + optionalDependencies: + '@unocss/webpack': 66.0.0(webpack@5.98.0) + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - postcss + - supports-color + - vue + unpipe@1.0.0: {} unplugin-utils@0.2.4: @@ -16748,10 +17792,10 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.2 - unplugin-vue-router@0.10.9(rollup@3.29.5)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)): + unplugin-vue-router@0.10.9(rollup@4.34.9)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)): dependencies: '@babel/types': 7.26.9 - '@rollup/pluginutils': 5.1.4(rollup@3.29.5) + '@rollup/pluginutils': 5.1.4(rollup@4.34.9) '@vue-macros/common': 1.16.1(vue@3.5.13(typescript@5.6.3)) ast-walker-scope: 0.6.2 chokidar: 3.6.0 @@ -16807,6 +17851,11 @@ snapshots: acorn: 8.14.1 webpack-virtual-modules: 0.6.2 + unplugin@2.2.2: + dependencies: + acorn: 8.14.1 + webpack-virtual-modules: 0.6.2 + unstorage@1.15.0(db0@0.3.1(@libsql/client@0.14.0)(better-sqlite3@11.8.1))(ioredis@5.6.0): dependencies: anymatch: 3.1.3 @@ -16916,14 +17965,28 @@ snapshots: vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vite-hot-client: 2.0.4(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vite-dev-rpc@1.0.7(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + birpc: 2.2.0 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-hot-client: 2.0.4(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + vite-hot-client@0.2.4(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): dependencies: vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-hot-client@0.2.4(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-hot-client@2.0.4(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): dependencies: vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-hot-client@2.0.4(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-node@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): dependencies: cac: 6.7.14 @@ -16945,6 +18008,27 @@ snapshots: - tsx - yaml + vite-node@3.1.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@9.4.0) + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-checker@0.9.0(eslint@9.22.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.6.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): dependencies: '@babel/code-frame': 7.26.2 @@ -16962,6 +18046,23 @@ snapshots: optionator: 0.9.4 typescript: 5.6.3 + vite-plugin-checker@0.9.1(eslint@9.22.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.6.3)(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + '@babel/code-frame': 7.26.2 + chokidar: 4.0.3 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.2 + strip-ansi: 7.1.0 + tiny-invariant: 1.3.3 + tinyglobby: 0.2.12 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vscode-uri: 3.1.0 + optionalDependencies: + eslint: 9.22.0(jiti@2.4.2) + optionator: 0.9.4 + typescript: 5.6.3 + vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.0(magicast@0.3.5))(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): dependencies: ansis: 3.17.0 @@ -16979,6 +18080,57 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.0(magicast@0.3.5))(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + ansis: 3.17.0 + debug: 4.4.0(supports-color@9.4.0) + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.1.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.4 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-dev-rpc: 1.0.7(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + optionalDependencies: + '@nuxt/kit': 3.16.0(magicast@0.3.5) + transitivePeerDependencies: + - supports-color + + vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + ansis: 3.17.0 + debug: 4.4.0(supports-color@9.4.0) + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.1.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.4 + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-dev-rpc: 1.0.7(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + optionalDependencies: + '@nuxt/kit': 3.16.1(magicast@0.3.5) + transitivePeerDependencies: + - supports-color + + vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)): + dependencies: + ansis: 3.17.0 + debug: 4.4.0(supports-color@9.4.0) + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.1.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.4 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite-dev-rpc: 1.0.7(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + optionalDependencies: + '@nuxt/kit': 3.16.1(magicast@0.3.5) + transitivePeerDependencies: + - supports-color + vite-plugin-vue-tracer@0.1.1(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)): dependencies: estree-walker: 3.0.3 @@ -16988,6 +18140,35 @@ snapshots: vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) vue: 3.5.13(typescript@5.6.3) + vite-plugin-vue-tracer@0.1.1(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)): + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.17 + pathe: 2.0.3 + source-map-js: 1.2.1 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.3) + + vite-plugin-vue-tracer@0.1.3(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)): + dependencies: + estree-walker: 3.0.3 + exsolve: 1.0.4 + magic-string: 0.30.17 + pathe: 2.0.3 + source-map-js: 1.2.1 + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.3) + + vite-plugin-vue-tracer@0.1.3(vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.3)): + dependencies: + estree-walker: 3.0.3 + exsolve: 1.0.4 + magic-string: 0.30.17 + pathe: 2.0.3 + source-map-js: 1.2.1 + vite: 6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.3) + vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): dependencies: esbuild: 0.25.1 @@ -17000,6 +18181,18 @@ snapshots: terser: 5.39.0 yaml: 2.7.0 + vite@6.2.4(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): + dependencies: + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.35.0 + optionalDependencies: + '@types/node': 22.13.10 + fsevents: 2.3.3 + jiti: 2.4.2 + terser: 5.39.0 + yaml: 2.7.0 + vitest-environment-nuxt@1.0.1(@types/node@22.13.10)(jiti@2.4.2)(magicast@0.3.5)(terser@5.39.0)(typescript@5.6.3)(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0): dependencies: '@nuxt/test-utils': 3.17.2(@types/node@22.13.10)(jiti@2.4.2)(magicast@0.3.5)(terser@5.39.0)(typescript@5.6.3)(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) @@ -17321,6 +18514,11 @@ snapshots: '@poppinss/exception': 1.2.0 error-stack-parser-es: 0.1.5 + youch-core@0.3.2: + dependencies: + '@poppinss/exception': 1.2.0 + error-stack-parser-es: 1.0.5 + youch@4.1.0-beta.6: dependencies: '@poppinss/dumper': 0.6.3 diff --git a/src/module.ts b/src/module.ts index 968505b4..28112104 100644 --- a/src/module.ts +++ b/src/module.ts @@ -156,6 +156,10 @@ export interface ModuleOptions { credits: boolean } +export interface ModuleRuntimeHooks { + 'robots:bot-detected': () => Promise | void +} + export interface ResolvedModuleOptions extends ModuleOptions { sitemap: string[] disallow: string[] @@ -246,6 +250,8 @@ export default defineNuxtModule({ if (config.metaTag) addPlugin({ mode: 'server', src: resolve('./runtime/app/plugins/robot-meta.server') }) + addPlugin({ src: resolve('./runtime/app/plugins/botd') }) + if (config.robotsTxt && config.mergeWithRobotsTxtPath !== false) { let usingRobotsTxtPath = '' let robotsTxt: boolean | string = false @@ -525,7 +531,7 @@ declare module 'h3' { handler: resolve('./runtime/server/middleware/injectContext'), }) addServerPlugin(resolve('./runtime/server/plugins/initContext')) - addServerPlugin(resolve('./runtime/server/plugins/botTracker')) + addServerPlugin(resolve('./runtime/server/plugins/botd')) if (isNuxtContentV2) { addServerHandler({ diff --git a/src/runtime/app/plugins/botd.ts b/src/runtime/app/plugins/botd.ts new file mode 100644 index 00000000..d0b71071 --- /dev/null +++ b/src/runtime/app/plugins/botd.ts @@ -0,0 +1,25 @@ +import { defineNuxtPlugin, onNuxtReady, useRequestEvent, useState } from 'nuxt/app' + +export default defineNuxtPlugin({ + async setup(nuxtApp) { + const isBot = useState('isBot', () => null) + if (import.meta.server) { + isBot.value = useRequestEvent()?.context?.isBot + } + // need a unique key to store the bot detection result + if (isBot.value) { + await nuxtApp.hooks.callHook('robots:bot-detected') + } + else if (import.meta.client) { + onNuxtReady(async () => { + const { load } = await import('@fingerprintjs/botd') + // Initialize an agent at application startup, once per page/app. + const isBot = (await load()).detect() + if (isBot) { + // persist isBot + await nuxtApp.hooks.callHook('robots:bot-detected') + } + }) + } + }, +}) diff --git a/src/runtime/server/lib/is-bot/behavior.ts b/src/runtime/server/lib/is-bot/behavior.ts new file mode 100644 index 00000000..d55c66c9 --- /dev/null +++ b/src/runtime/server/lib/is-bot/behavior.ts @@ -0,0 +1,958 @@ +// src/runtime/server/lib/botd.ts +import type { getHeaders, H3Event } from 'h3' +import { useRuntimeConfig } from '#imports' +import { useSession } from 'h3' + +// Common sensitive paths that bots target - expanded with more patterns +export const SENSITIVE_PATHS = [ + '/wp-login', + '/xmlrpc.php', + '/.env', + '/phpmyadmin', + '/setup', + '/install', + '/config', + '/.git', + '/.svn', + '/api/graphql', + '/graphql', + // Additional common bot targets + '/wp-content', + '/wp-includes', + '/wp-json', + '/.well-known/security.txt', + '/vendor/', + '/server-status', + '/solr/', + '/jenkins/', + '/.DS_Store', + '/actuator/', + '/console/', + '/wp-admin', + '/admin-login.php', + '/wp-login-hidden', +] + +// Honeypot/high-sensitivity paths - these could be legitimate in some cases +// but are frequently targeted by bots and rarely used by regular users +export const MAYBE_SENSITIVE_PATHS = [ + '/admin', + '/login', + '/administrator', + '/includes/config', + '/.hidden-login', + '/robots.txt.bak', + '/administrator/index.php', + '/myadmin', + '/admin_area', + '/panel', + '/cpanel', + '/dashboard', +] + +// Enhanced bot detection score thresholds with an intermediate level +export const BOT_SCORE_THRESHOLDS = { + DEFINITELY_BOT: 90, + LIKELY_BOT: 70, + SUSPICIOUS: 40, + PROBABLY_HUMAN: 20, + DEFINITELY_HUMAN: 5, +} + +// Updated behavior weights with increased penalties for timing issues +export const BEHAVIOR_WEIGHTS = { + SENSITIVE_PATH: 15, // Accessing known sensitive paths + MAYBE_SENSITIVE_PATH: 5, // Accessing potentially sensitive paths (honeypot/admin areas) + RAPID_REQUESTS: 20, // Too many requests in short time + REPEATED_ERRORS: 15, // Repeated 404s or errors + UNUSUAL_PATTERN: 25, // Unusual access pattern + NONEXISTENT_RESOURCES: 10, // Requesting resources that don't exist + REQUEST_CONSISTENCY: 20, // Consistency in request patterns + MULTIPLE_SENSITIVE_HITS: 40, // Multiple hits to different sensitive paths + RESOURCE_TIMING: 25, // Abnormal timing between resource requests (increased from 15) + SESSION_ANOMALY: 30, // Suspicious session behavior +} + +// Traffic classification - helps distinguish between different user types +export enum TrafficType { + REGULAR_USER = 'regular_user', + SUSPICIOUS = 'suspicious_bot', + MALICIOUS_BOT = 'malicious_bot', + UNKNOWN = 'unknown', +} + +// Enhanced session data with more behavioral indicators +export interface SessionData { + lastRequests: Array<{ + timestamp: number + path: string + status?: number + timeSincePrevious?: number + method?: string + }> + suspiciousPathHits: number + maybeSensitivePathHits: number + uniqueSensitivePathsAccessed: string[] // Track unique sensitive paths accessed + errorCount: number + score: number + lastUpdated: number + trafficType: TrafficType + knownGoodActions: number // Count of actions that indicate human behavior + tempExemptUntil?: number // Timestamp for temporary exemption + requestMethodVariety: string[] // Array of used HTTP methods + averageTimeBetweenRequests?: number + requestSequenceEntropy: number // Measure of randomness in request sequence + firstSeenAt: number // When the session was first created + behaviorChangePoints?: number[] // Timestamps where behavior significantly changed +} + +export interface IPData { + sessionCount: number + activeSessions: string[] // Track active session IDs + suspiciousScore: number + lastUpdated: number + whitelisted: boolean + blacklisted: boolean + legitSessionsCount: number // Count of sessions that passed human verification + sessionsPerHour?: number // Rate of new sessions creation + lastSessionCreated?: number // Timestamp of the last session created +} + +// More sophisticated HTML path detection +export function isLikelyHtmlPath(path: string, headers?: Record): boolean { + // Parse accept header properly with quality values + if (headers?.accept) { + // Check if HTML is explicitly in the accept header with priority + const acceptTypes = headers.accept.split(',') + for (const type of acceptTypes) { + const [mimeType, quality] = type.split(';') + const trimmedType = mimeType.trim() + + // If html is explicitly requested with high quality value + if ((trimmedType === 'text/html' || trimmedType === 'application/xhtml+xml') + && (!quality || Number.parseFloat(quality.replace('q=', '')) > 0.5)) { + return true + } + } + + // If accept header exists but doesn't prioritize HTML, check it against typical API/asset patterns + if (headers.accept.includes('application/json') + || headers.accept.includes('image/') + || headers.accept === '*/*') { + // Probably not an HTML request + return false + } + } + + // Check common URL patterns that indicate non-HTML resources + // Skip common non-HTML file extensions + const nonHtmlExtensions = [ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.webp', + '.svg', + '.ico', + '.css', + '.js', + '.json', + '.map', + '.woff', + '.woff2', + '.ttf', + '.eot', + '.mp4', + '.webm', + '.mp3', + '.wav', + '.pdf', + '.xml', + '.zip', + '.gz', + '.tar', + '.wasm', + '.avif', + '.heic', + '.webmanifest', + '.txt', + ] + + // Check if path ends with a non-HTML extension + const pathLower = path.toLowerCase() + if (nonHtmlExtensions.some(ext => pathLower.endsWith(ext))) { + return false + } + + // Check for API endpoints or static asset paths which aren't typically HTML + const nonHtmlPatterns = [ + '/api/', + '/_nuxt/', + '/_next/', + '/assets/', + '/static/', + '/img/', + '/images/', + '/media/', + '/fonts/', + '/dist/', + '/public/', + '/cdn/', + '/uploads/', + '/scripts/', + '/styles/', + ] + + if (nonHtmlPatterns.some(pattern => pathLower.includes(pattern))) { + return false + } + + // Look for query parameters that suggest non-HTML responses + if (path.includes('?') + && (path.includes('format=json') + || path.includes('output=json') + || path.includes('.json'))) { + return false + } + + // Default to treating it as HTML if no specific non-HTML indicators are present + // especially paths with no extension or paths ending in / which are likely pages + return true +} + +export async function useBotDetectionSession(event: H3Event) { + const config = useRuntimeConfig() + return await useSession(event, { + password: config.robots.botDetectionSecret || '80d42cfb-1cd2-462c-8f17-e3237d9027e9', + }) +} + +// Helper to check if path is in the maybe-sensitive category +export function isMaybeSensitivePath(path: string): boolean { + return MAYBE_SENSITIVE_PATHS.some(sp => path.includes(sp)) +} + +// Calculate entropy of request sequences to detect non-human patterns +function calculateRequestEntropy(paths: string[]): number { + if (paths.length < 3) + return 0 + + // Count occurrences of each path + const pathCounts: Record = {} + for (const path of paths) { + pathCounts[path] = (pathCounts[path] || 0) + 1 + } + + // Calculate entropy + let entropy = 0 + const totalPaths = paths.length + + for (const path in pathCounts) { + const probability = pathCounts[path] / totalPaths + entropy -= probability * Math.log2(probability) + } + + return entropy +} + +// Detect if a request sequence matches natural browsing patterns +function analyzeRequestSequence(requests: Array<{ path: string, timestamp: number }>): { + isNaturalBrowsing: boolean + entropy: number + timeConsistency: number +} { + if (requests.length < 5) { + return { isNaturalBrowsing: true, entropy: 0, timeConsistency: 1 } + } + + // 1. Check time intervals between requests + const intervals: number[] = [] + + // Sort requests by timestamp (oldest to newest) for interval calculation + const sortedRequests = [...requests].sort((a, b) => a.timestamp - b.timestamp) + + for (let i = 1; i < sortedRequests.length; i++) { + intervals.push(sortedRequests[i].timestamp - sortedRequests[i - 1].timestamp) + } + + // Calculate variance in intervals + const avgInterval = intervals.reduce((sum, val) => sum + val, 0) / intervals.length + const variance = intervals.reduce((sum, val) => sum + (val - avgInterval) ** 2, 0) / intervals.length + + // Natural browsing has some variance in timing + const timeConsistency = Math.min(1, variance / (avgInterval * avgInterval)) + + // 2. Calculate path entropy + const paths = requests.map(r => r.path) + const entropy = calculateRequestEntropy(paths) + + // 3. Multiple indicators of unnatural browsing patterns + let suspiciousPatternCount = 0 + + // A. Check for alphabetical or sequential scanning patterns + const pathsInOrder = sortedRequests.map(r => r.path) + let sequentialOrdering = true + + for (let i = 1; i < pathsInOrder.length; i++) { + // If paths don't progress in a way that suggests scanning, it's more natural + if (pathsInOrder[i].localeCompare(pathsInOrder[i - 1]) < 0) { + sequentialOrdering = false + break + } + } + + if (sequentialOrdering && pathsInOrder.length >= 4) { + suspiciousPatternCount++ + } + + // B. Check for common prefix patterns (like crawling similar endpoints) + const pathPrefixes = new Map() + for (const path of paths) { + // Extract the first segment of the path (e.g., "/admin" from "/admin/users") + const prefix = `/${path.split('/')[1]}` + pathPrefixes.set(prefix, (pathPrefixes.get(prefix) || 0) + 1) + } + + // If 80% of requests are to the same prefix, it's suspicious + for (const [, count] of pathPrefixes.entries()) { + if (count >= Math.ceil(paths.length * 0.8) && paths.length >= 4) { + suspiciousPatternCount++ + break + } + } + + // C. Look for numeric incrementation patterns in paths (like id scanning) + const hasNumericPattern = paths.some((path) => { + const matches = path.match(/(\d+)/g) + return matches && matches.length > 0 + }) + + if (hasNumericPattern) { + let numericSequential = true + const numericValues: number[] = [] + + // Extract numeric values from paths + for (const path of pathsInOrder) { + const matches = path.match(/(\d+)/g) + if (matches && matches.length > 0) { + numericValues.push(Number.parseInt(matches[0], 10)) + } + else { + numericSequential = false + break + } + } + + // Check if numeric values are sequential or have a pattern + if (numericSequential && numericValues.length >= 3) { + let hasPattern = true + const diff = numericValues[1] - numericValues[0] + + for (let i = 2; i < numericValues.length; i++) { + if (numericValues[i] - numericValues[i - 1] !== diff) { + hasPattern = false + break + } + } + + if (hasPattern) { + suspiciousPatternCount++ + } + } + } + + // D. Check for consistent path length/structure (indicative of automation) + const pathLengths = paths.map(p => p.length) + const avgLength = pathLengths.reduce((sum, len) => sum + len, 0) / pathLengths.length + const lengthVariance = pathLengths.reduce((sum, len) => sum + (len - avgLength) ** 2, 0) / pathLengths.length + + // If path lengths are very consistent, it's suspicious + if (Math.sqrt(lengthVariance) / avgLength < 0.1 && paths.length >= 4) { + suspiciousPatternCount++ + } + + // Combine indicators to determine if this is natural browsing + const isNaturalBrowsing = ( + // Either high entropy (varied paths) and some time variance + (entropy > 1.5 && timeConsistency > 0.2) + // Or no suspicious patterns detected + || suspiciousPatternCount === 0 + ) + + return { + isNaturalBrowsing, + entropy, + timeConsistency, + } +} + +// Helper function to identify adaptive rate limits based on client history +function calculateRateLimit(sessionData: SessionData): number { + // Base rate limit - start with a moderate default + let rateLimit = 15 // requests per minute + + // Adjust based on client behavior + if (sessionData.trafficType === TrafficType.REGULAR_USER) { + // Regular users can have higher bursts during normal browsing + rateLimit = 30 + } + else if (sessionData.suspiciousPathHits > 0) { + // Clients that hit suspicious paths get stricter limits + rateLimit = Math.max(5, rateLimit - sessionData.suspiciousPathHits * 2) + } + + // Adjust for known good behavior + if (sessionData.knownGoodActions > 5) { + // Clients with good history get more flexibility + rateLimit += Math.min(20, sessionData.knownGoodActions) + } + + return rateLimit +} + +// Detect potential session hijacking or cookie theft +function detectSessionAnomaly(ipData: IPData, sessionData: SessionData, timestamp: number = Date.now()): { + suspicious: boolean + severity: number + reason?: string +} { + const result = { + suspicious: false, + severity: 0, + reason: '', + } + + const SESSION_AGE_THRESHOLD = 24 * 60 * 60 * 1000 // 24 hours in milliseconds + const now = timestamp + + // Check for IP with many sessions + if (ipData.activeSessions.length > 10) { + result.suspicious = true + result.severity = Math.min(70, ipData.activeSessions.length * 5) + result.reason = 'MANY_SESSIONS' + return result + } + + // Check for high session creation rate + if (ipData.sessionsPerHour && ipData.sessionsPerHour > 5) { + result.suspicious = true + result.severity = Math.min(60, ipData.sessionsPerHour * 10) + result.reason = 'RAPID_SESSION_CREATION' + return result + } + + // Check for abrupt behavior changes in old sessions + if (sessionData.firstSeenAt && (now - sessionData.firstSeenAt > SESSION_AGE_THRESHOLD)) { + // Old session with sudden suspicious activity + if (sessionData.suspiciousPathHits > 0 && sessionData.lastRequests.length > 5) { + // Check if suspicious behavior started recently + const recentRequests = sessionData.lastRequests.slice(-5) + const olderRequests = sessionData.lastRequests.slice(0, -5) + + // Calculate scores for both segments + const recentSuspicious = recentRequests.filter(r => + SENSITIVE_PATHS.some(sp => r.path.includes(sp)) + || MAYBE_SENSITIVE_PATHS.some(sp => r.path.includes(sp)), + ).length + + const olderSuspicious = olderRequests.filter(r => + SENSITIVE_PATHS.some(sp => r.path.includes(sp)) + || MAYBE_SENSITIVE_PATHS.some(sp => r.path.includes(sp)), + ).length + + // If recent behavior is much more suspicious than older behavior + if (recentSuspicious > 0 && (olderSuspicious === 0 || recentSuspicious / olderRequests.length > olderSuspicious / olderRequests.length * 3)) { + result.suspicious = true + result.severity = 50 + result.reason = 'BEHAVIOR_CHANGE' + + // Mark this point as a behavior change point + if (!sessionData.behaviorChangePoints) { + sessionData.behaviorChangePoints = [] + } + sessionData.behaviorChangePoints.push(now) + } + } + } + + return result +} + +export async function analyzeBotBehavior({ + ip, + path, + method, + storage, + session, + headers, + timestamp = Date.now(), +}: { + ip: string + path: string + method: string + storage: any // The kvStorage interface + session: { id: string } + headers: ReturnType + timestamp?: number +}) { + // Check if this is an HTML path - if not, skip extensive bot detection + const isHtmlPath = isLikelyHtmlPath(path, { accept: headers.accept || '' }) + + // Check if this is a maybe-sensitive path + const isMaybeSensitive = isMaybeSensitivePath(path) + const now = timestamp + const sessionKey = `session:${session.id}` + const ipKey = `ip:${ip}` + + // Initialize or get session data with improved defaults + const sessionData: SessionData = await storage.getItem(sessionKey) || { + lastRequests: [], + suspiciousPathHits: 0, + maybeSensitivePathHits: 0, + uniqueSensitivePathsAccessed: [], + errorCount: 0, + score: 0, + lastUpdated: now, + trafficType: TrafficType.UNKNOWN, + knownGoodActions: 0, + requestMethodVariety: [], + requestSequenceEntropy: 0, + firstSeenAt: now, + } + + // Initialize or get IP data with improved defaults + const ipData: IPData = await storage.getItem(ipKey) || { + sessionCount: 0, + activeSessions: [], + suspiciousScore: 0, + lastUpdated: now, + whitelisted: false, + blacklisted: false, + legitSessionsCount: 0, + lastSessionCreated: now, + } + + // Check for whitelist/blacklist + if (ipData.whitelisted) { + return { + score: 0, + ipScore: 0, + isLikelyBot: false, + factors: {}, + session: session.id, + ip, + whitelisted: true, + } + } + + if (ipData.blacklisted) { + return { + score: 100, + ipScore: 100, + isLikelyBot: true, + factors: { BLACKLISTED: 100 }, + session: session.id, + ip, + blacklisted: true, + } + } + + // Calculate scoring factors + const scoreFactors: Record = {} + + // Check for maybe-sensitive path access + if (isMaybeSensitive) { + sessionData.maybeSensitivePathHits = (sessionData.maybeSensitivePathHits || 0) + 1 + + // Track unique sensitive paths for detecting scanning behavior + sessionData.uniqueSensitivePathsAccessed = sessionData.uniqueSensitivePathsAccessed || [] + if (!sessionData.uniqueSensitivePathsAccessed.includes(path)) { + sessionData.uniqueSensitivePathsAccessed.push(path) + } + + // Apply score - smaller penalty for first hit, larger for repeated behavior + if (sessionData.maybeSensitivePathHits === 1) { + scoreFactors.MAYBE_SENSITIVE_PATH = BEHAVIOR_WEIGHTS.MAYBE_SENSITIVE_PATH + } + else if (sessionData.maybeSensitivePathHits > 1) { + // Multiple hits to sensitive paths is more suspicious + scoreFactors.MAYBE_SENSITIVE_PATH = BEHAVIOR_WEIGHTS.MAYBE_SENSITIVE_PATH + * Math.min(3, sessionData.maybeSensitivePathHits) + + // If they hit multiple different sensitive paths, that's even more suspicious + if (sessionData.uniqueSensitivePathsAccessed.length >= 2) { + scoreFactors.MULTIPLE_SENSITIVE_HITS = BEHAVIOR_WEIGHTS.MULTIPLE_SENSITIVE_HITS + } + } + } + + // Calculate time since previous request if applicable + let timeSincePrevious = 0 + if (sessionData.lastRequests.length > 0) { + timeSincePrevious = now - sessionData.lastRequests[sessionData.lastRequests.length - 1].timestamp + } + + // Track this request with enhanced metadata + sessionData.lastRequests.push({ + timestamp: now, + path, + method, + timeSincePrevious, + }) + + // Track HTTP method variety + if (!sessionData.requestMethodVariety.includes(method)) { + sessionData.requestMethodVariety.push(method) + } + + // Only keep last 30 requests for better pattern analysis + if (sessionData.lastRequests.length > 30) { + sessionData.lastRequests.shift() + } + + // For non-HTML paths, only do minimal processing + if (!isHtmlPath && !isMaybeSensitive) { + // Still save the request data for context + sessionData.lastUpdated = now + // await storage.setItem(sessionKey, sessionData, { + // ttl: 60 * 60 * 24, // 24 hour TTL + // }) + + // Associate this session with the IP if not already tracked + if (!ipData.activeSessions.includes(session.id)) { + ipData.activeSessions.push(session.id) + ipData.sessionCount = ipData.activeSessions.length + + // Calculate session creation rate + if (ipData.lastSessionCreated) { + const hoursSinceLastSession = (now - ipData.lastSessionCreated) / (1000 * 60 * 60) + + if (hoursSinceLastSession < 1) { + // If creating sessions more than once per hour, track the rate + ipData.sessionsPerHour = ipData.sessionsPerHour + ? (ipData.sessionsPerHour * 0.7 + (1 / hoursSinceLastSession) * 0.3) // Weighted average + : (1 / hoursSinceLastSession) + } + } + ipData.lastSessionCreated = now + + // await storage.setItem(ipKey, ipData, { + // ttl: 60 * 60 * 24 * 7, // 7 day TTL for IP data + // }) + } + + // Return current scores without additional processing + return { + score: sessionData.score, + ipScore: ipData.suspiciousScore, + isLikelyBot: sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT, + factors: {}, + session: session.id, + ip, + skippedProcessing: true, + } + } + + // Apply time decay to previous scores (reduce by ~10% per hour) + const hoursSinceLastUpdate = (now - sessionData.lastUpdated) / (1000 * 60 * 60) + if (hoursSinceLastUpdate > 0) { + sessionData.score = Math.max(0, sessionData.score * 0.9 ** hoursSinceLastUpdate) + } + + // Associate this session with the IP if not already tracked + if (!ipData.activeSessions.includes(session.id)) { + ipData.activeSessions.push(session.id) + ipData.sessionCount = ipData.activeSessions.length + + // Calculate session creation rate + if (ipData.lastSessionCreated) { + const hoursSinceLastSession = (now - ipData.lastSessionCreated) / (1000 * 60 * 60) + + if (hoursSinceLastSession < 1) { + // If creating sessions more than once per hour, track the rate + ipData.sessionsPerHour = ipData.sessionsPerHour + ? (ipData.sessionsPerHour * 0.7 + (1 / hoursSinceLastSession) * 0.3) // Weighted average + : (1 / hoursSinceLastSession) + } + } + ipData.lastSessionCreated = now + } + + // 1. Check for sensitive path access + if (SENSITIVE_PATHS.some(sensitivePath => path.includes(sensitivePath))) { + sessionData.suspiciousPathHits++ + scoreFactors.SENSITIVE_PATH = BEHAVIOR_WEIGHTS.SENSITIVE_PATH + } + + // 2. Check for rapid requests with adaptive rate limiting + const oneMinuteAgo = now - 60000 + const requestsLastMinute = sessionData.lastRequests.filter(req => req.timestamp > oneMinuteAgo).length + const adaptiveRateLimit = calculateRateLimit(sessionData) + + if (requestsLastMinute > adaptiveRateLimit) { + // Apply score proportional to how much the limit was exceeded + const overageRatio = requestsLastMinute / adaptiveRateLimit + scoreFactors.RAPID_REQUESTS = Math.min( + BEHAVIOR_WEIGHTS.RAPID_REQUESTS * overageRatio, + BEHAVIOR_WEIGHTS.RAPID_REQUESTS * 2, // Cap at double the weight + ) + } + + // 3. Analyze request sequence for natural browsing patterns + if (sessionData.lastRequests.length >= 5) { + const sequenceAnalysis = analyzeRequestSequence(sessionData.lastRequests) + sessionData.requestSequenceEntropy = sequenceAnalysis.entropy + + if (!sequenceAnalysis.isNaturalBrowsing) { + scoreFactors.UNUSUAL_PATTERN = BEHAVIOR_WEIGHTS.UNUSUAL_PATTERN + * (1 - Math.min(1, sequenceAnalysis.entropy / 2)) + } + else { + // Reduce score for natural browsing patterns - positive reinforcement + sessionData.score = Math.max(0, sessionData.score - 5) + sessionData.knownGoodActions += 1 + } + } + + // 4. Check for session anomaly - add anomaly detection logic here + const sessionAnomaly = detectSessionAnomaly(ipData, sessionData, timestamp) + if (sessionAnomaly.suspicious) { + scoreFactors.SESSION_ANOMALY = Math.min( + BEHAVIOR_WEIGHTS.SESSION_ANOMALY, + sessionAnomaly.severity, + ) + } + + // 5. Check request timing consistency + // Bots often have very consistent intervals between requests + if (sessionData.lastRequests.length > 5) { + // Only analyze the existing requests, not including the one just added + // This prevents the new request from breaking the pattern analysis + const existingRequests = sessionData.lastRequests.slice(0, -1) + + // Extract intervals only from requests that have a valid timeSincePrevious value + const intervals = [] + for (let i = 0; i < existingRequests.length; i++) { + if (existingRequests[i].timeSincePrevious && existingRequests[i].timeSincePrevious > 0) { + intervals.push(existingRequests[i].timeSincePrevious) + } + } + + // Only proceed if we have enough intervals to analyze + if (intervals.length >= 4) { + // Calculate mean and standard deviation + const mean = intervals.reduce((sum, val) => sum + val, 0) / intervals.length + const variance = intervals.reduce((sum, val) => sum + (val - mean) ** 2, 0) / intervals.length + const stdDev = Math.sqrt(variance) + + // Very low standard deviation indicates suspiciously consistent timing + const coefficientOfVariation = stdDev / mean + + // Enhanced scoring logic with multiple tiers of suspicion: + + // Extremely precise timing (practically impossible for humans) + if (coefficientOfVariation < 0.05 && intervals.length >= 5) { + // This is absolutely a bot - humans cannot maintain this precision + scoreFactors.RESOURCE_TIMING = BEHAVIOR_WEIGHTS.RESOURCE_TIMING * 3 // Triple the weight + + // If very fast as well (sub-second), even more suspicious + if (mean < 1000) { + scoreFactors.RESOURCE_TIMING += 15 // Additional penalty for inhuman speed + } + } + // Very suspicious timing + else if (coefficientOfVariation < 0.1 && mean < 2000) { + // Highly suspicious but not impossible + scoreFactors.RESOURCE_TIMING = BEHAVIOR_WEIGHTS.RESOURCE_TIMING * 2 // Double the weight + } + // Somewhat suspicious timing + else if (coefficientOfVariation < 0.2 && mean < 3000) { + // Still suspicious but less so + scoreFactors.RESOURCE_TIMING = BEHAVIOR_WEIGHTS.RESOURCE_TIMING + } + + // Update average time between requests for future analysis + sessionData.averageTimeBetweenRequests = mean + } + } + + // Add up all score factors + const additionalScore = Object.values(scoreFactors).reduce((sum, val) => sum + val, 0) + sessionData.score += additionalScore + + // Update traffic type classification based on score + if (sessionData.score >= BOT_SCORE_THRESHOLDS.DEFINITELY_BOT) { + sessionData.trafficType = TrafficType.MALICIOUS_BOT + } + else if (sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT) { + sessionData.trafficType = TrafficType.SUSPICIOUS + } + else if (sessionData.score >= BOT_SCORE_THRESHOLDS.SUSPICIOUS) { + sessionData.trafficType = TrafficType.SUSPICIOUS + } + else if (sessionData.score <= BOT_SCORE_THRESHOLDS.PROBABLY_HUMAN) { + sessionData.trafficType = TrafficType.REGULAR_USER + } + + // Cap score at 100 + sessionData.score = Math.min(100, sessionData.score) + + // Update IP score based on session score with memory effect + // This allows the IP to be marked as suspicious based on behavior across multiple sessions + ipData.suspiciousScore = Math.max( + ipData.suspiciousScore * 0.9, // Decay previous score + sessionData.score * 0.8, // Influence from current session + ) + + // Increment legitimate session count if this seems to be a real user + if (sessionData.score <= BOT_SCORE_THRESHOLDS.PROBABLY_HUMAN + && sessionData.knownGoodActions >= 3) { + ipData.legitSessionsCount++ + + // If an IP has many legitimate sessions, gradually reduce its suspicious score + if (ipData.legitSessionsCount > 5 && ipData.suspiciousScore > 0) { + ipData.suspiciousScore = Math.max(0, ipData.suspiciousScore - 5) + } + } + + // Save data back to storage + sessionData.lastUpdated = now + ipData.lastUpdated = now + + // Return enhanced bot detection result + return { + score: sessionData.score, + ipScore: ipData.suspiciousScore, + isLikelyBot: sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT, + factors: scoreFactors, + trafficType: sessionData.trafficType, + session: session.id, + ip, + sensitivePaths: sessionData.uniqueSensitivePathsAccessed || [], + requestEntropy: sessionData.requestSequenceEntropy, + legitActions: sessionData.knownGoodActions, + sessionAge: now - sessionData.firstSeenAt, + behaviorChanges: sessionData.behaviorChangePoints?.length || 0, + ipKey, + ipData, + sessionKey, + sessionData, + } +} + +// Enhanced bot detection with improved behavior analysis + +// Update bot score after request completion (to account for status codes) +export async function updateBotScoreAfterRequest(event: H3Event, status: number, storage: any, session, any) { + const sessionData: SessionData | null = await kvStorage.getItem(sessionKey) + if (!sessionData) + return + + // Update the last request with the status code + if (sessionData.lastRequests.length > 0) { + sessionData.lastRequests[sessionData.lastRequests.length - 1].status = status + } + + // For non-HTML paths, just update the status code without scoring + const headers = getHeaders(event) + const acceptHeader = headers.accept || '' + if (!isLikelyHtmlPath(path, { accept: acceptHeader }) && !isMaybeSensitivePath(path)) { + await kvStorage.setItem(sessionKey, sessionData) + return + } + + // Count errors (404s, 403s, etc.) + if (status >= 400) { + sessionData.errorCount++ + + // Add score for repeated errors with progressive penalty + if (sessionData.errorCount > 2) { + // Apply increasing penalty for each error after the first few + const errorPenalty = Math.min( + BEHAVIOR_WEIGHTS.REPEATED_ERRORS, + BEHAVIOR_WEIGHTS.REPEATED_ERRORS * (sessionData.errorCount - 2) / 5, + ) + sessionData.score += errorPenalty + + // Check for consecutive errors + const recentRequests = sessionData.lastRequests.slice(-5) + const consecutiveErrors = recentRequests.filter(req => req.status && req.status >= 400).length + + if (consecutiveErrors >= 3) { + // Strong bot signal: consecutive errors + sessionData.score += BEHAVIOR_WEIGHTS.REPEATED_ERRORS + + // If a session shows significant behavior changes, make note of this + if (!sessionData.behaviorChangePoints) { + sessionData.behaviorChangePoints = [] + } + + // If we have a consecutive error pattern, consider this a behavior change point + sessionData.behaviorChangePoints.push(Date.now()) + } + } + + // 404s might indicate scanning for vulnerabilities + if (status === 404) { + const pathIsCommonResource = path.endsWith('.js') + || path.endsWith('.css') + || path.includes('/images/') + || path.includes('/assets/') + + // Don't penalize 404s on common resource paths as much (could be stale cache or changed resource names) + if (!pathIsCommonResource) { + sessionData.score += BEHAVIOR_WEIGHTS.NONEXISTENT_RESOURCES + } + else { + // Apply smaller penalty for resource 404s + sessionData.score += BEHAVIOR_WEIGHTS.NONEXISTENT_RESOURCES * 0.3 + } + } + + // Cap score at 100 + sessionData.score = Math.min(100, sessionData.score) + + // Update traffic type if needed + if (sessionData.score >= BOT_SCORE_THRESHOLDS.DEFINITELY_BOT) { + sessionData.trafficType = TrafficType.MALICIOUS_BOT + } + else if (sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT) { + sessionData.trafficType = TrafficType.SUSPICIOUS + } + + // Save updated data + await kvStorage.setItem(sessionKey, sessionData) + } + else if (status >= 200 && status < 300) { + // Successful requests may indicate legitimate use + // Especially 2xx on HTML pages + if (isLikelyHtmlPath(path, { accept: acceptHeader })) { + // Slightly reduce score for successful HTML page views + sessionData.score = Math.max(0, sessionData.score - 1) + sessionData.knownGoodActions += 0.5 + await kvStorage.setItem(sessionKey, sessionData) + } + } + + // Update IP storage if the score changed significantly + if (Math.abs(sessionData.score - (sessionData.lastScore || 0)) > 10) { + const ip = getRequestIP(event) + const ipKey = `ip:${ip}` + const ipData: IPData | null = await kvStorage.getItem(ipKey) + + if (ipData) { + // If this session suddenly became very suspicious, update IP score immediately + if (sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT + && (sessionData.lastScore || 0) < BOT_SCORE_THRESHOLDS.SUSPICIOUS) { + ipData.suspiciousScore = Math.max(ipData.suspiciousScore, sessionData.score * 0.8) + await kvStorage.setItem(ipKey, ipData, { + ttl: 60 * 60 * 24 * 7, // 7 day TTL for IP data + }) + } + } + + // Remember the last score for future comparisons + sessionData.lastScore = sessionData.score + await kvStorage.setItem(sessionKey, sessionData) + } +} diff --git a/src/runtime/server/lib/is-bot/userAgent.ts b/src/runtime/server/lib/is-bot/userAgent.ts new file mode 100644 index 00000000..fb13db12 --- /dev/null +++ b/src/runtime/server/lib/is-bot/userAgent.ts @@ -0,0 +1,331 @@ +// src/runtime/server/lib/botd.ts +import type { getHeaders } from 'h3' + +// Bot types classification with more detailed patterns +export const KNOWN_SEARCH_BOTS = [ + { + pattern: 'googlebot', + name: 'googlebot', + secondaryPatterns: ['google.com/bot.html'], + }, + { + pattern: 'bingbot', + name: 'bingbot', + secondaryPatterns: ['msnbot'], + }, + { + pattern: 'yandexbot', + name: 'yandexbot', + }, + { + pattern: 'baiduspider', + name: 'baiduspider', + secondaryPatterns: ['baidu.com'], + }, + { + pattern: 'duckduckbot', + name: 'duckduckbot', + secondaryPatterns: ['duckduckgo.com'], + }, + { + pattern: 'slurp', + name: 'yahoo', + }, +] + +export const SOCIAL_BOTS = [ + { + pattern: 'twitterbot', + name: 'twitter', + secondaryPatterns: ['twitter'], + }, + { + pattern: 'facebookexternalhit', + name: 'facebook', + secondaryPatterns: ['facebook.com'], + }, + { + pattern: 'linkedinbot', + name: 'linkedin', + secondaryPatterns: ['linkedin'], + }, + { + pattern: 'pinterestbot', + name: 'pinterest', + secondaryPatterns: ['pinterest'], + }, + { + pattern: 'discordbot', + name: 'discord', + secondaryPatterns: ['discordapp'], + }, +] + +export const SEO_BOTS = [ + { + pattern: 'mj12bot', + name: 'majestic12', + secondaryPatterns: ['majestic12.co.uk/bot'], + }, + { + pattern: 'ahrefsbot', + name: 'ahrefs', + secondaryPatterns: ['ahrefs.com'], + }, + { + pattern: 'semrushbot', + name: 'semrush', + secondaryPatterns: ['semrush.com/bot'], + }, + { + pattern: 'screaming frog', + name: 'screaming-frog', + secondaryPatterns: ['screamingfrog.co.uk'], + }, + { + pattern: 'rogerbot', + name: 'moz', + }, +] + +export const AI_BOTS = [ + { + pattern: 'anthropic', + name: 'anthropic', + }, + { + pattern: 'claude', + name: 'claude', + }, + { + pattern: 'gptbot', + name: 'gpt', + secondaryPatterns: ['openai.com'], + }, + { + pattern: 'googlebot-news', + name: 'google-news', + }, + { + pattern: 'cohere', + name: 'cohere', + secondaryPatterns: ['cohere.com'], + }, + { + pattern: 'ccbot', + name: 'commoncrawl', + secondaryPatterns: ['commoncrawl.org'], + }, + { + pattern: 'perplexitybot', + name: 'perplexity', + secondaryPatterns: ['perplexity.ai'], + }, +] +// Basic HTTP clients and tools +export const HTTP_TOOL_BOTS = [ + { + pattern: 'python-requests', + name: 'requests', + secondaryPatterns: ['python'], + }, + { + pattern: 'wget', + name: 'wget', + }, + { + pattern: 'curl', + name: 'curl', + secondaryPatterns: ['curl'], + }, +] + +export const SECURITY_SCANNING_BOTS = [ + { + pattern: 'zgrab', + name: 'zgrab', + }, + { + pattern: 'masscan', + name: 'masscan', + }, + { + pattern: 'nmap', + name: 'nmap', + secondaryPatterns: ['insecure.org'], + }, + { + pattern: 'nikto', + name: 'nikto', + }, + { + pattern: 'wpscan', + name: 'wpscan', + }, +] + +// Web scraping tools +export const SCRAPING_BOTS = [ + { + pattern: 'scrapy', + name: 'scrapy', + secondaryPatterns: ['scrapy.org'], + }, +] + +// Browser automation tools +export const AUTOMATION_BOTS = [ + { + pattern: 'phantomjs', + name: 'phantomjs', + }, + { + pattern: 'headless', + name: 'headless-browser', + }, + { + pattern: 'playwright', + name: 'playwright', + }, + { + pattern: 'selenium', + name: 'selenium', + secondaryPatterns: ['webdriver'], + }, + { + pattern: 'puppeteer', + name: 'puppeteer', + secondaryPatterns: ['headless'], + }, +] + +export const GENERIC_BOTS = [ + { + pattern: 'bot', + name: 'generic-bot', + }, + { + pattern: 'spider', + name: 'generic-spider', + }, + { + pattern: 'crawler', + name: 'generic-crawler', + }, + { + pattern: 'scraper', + name: 'generic-scraper', + }, +] + +export function isBotFromHeaders(headers: ReturnType): { + isBot: boolean + data?: { + botType: string + botName: string + suspiciousHeaders?: string[] + } +} { + const userAgent = headers['user-agent'] + const suspiciousHeaders: string[] = [] + + // Check for missing user-agent + if (!userAgent) { + return { + isBot: true, + data: { + botType: 'unknown', + botName: 'unknown', + suspiciousHeaders: ['missing-user-agent'], + }, + } + } + + const userAgentLower = userAgent.toLowerCase() + + // Check for header inconsistencies + const accept = headers.accept + const acceptLanguage = headers['accept-language'] + const acceptEncoding = headers['accept-encoding'] + + // Browsers typically send these headers + if (!accept) + suspiciousHeaders.push('missing-accept') + if (!acceptLanguage) + suspiciousHeaders.push('missing-accept-language') + if (!acceptEncoding) + suspiciousHeaders.push('missing-accept-encoding') + + // Check for mobile/desktop inconsistencies + const isMobileUserAgent = /mobile|android|iphone|ipad|ipod/i.test(userAgentLower) + const hasSecChUa = headers['sec-ch-ua'] + const hasSecChUaMobile = headers['sec-ch-ua-mobile'] + + if (isMobileUserAgent && hasSecChUaMobile === '?0') { + suspiciousHeaders.push('mobile-ua-desktop-client-hint') + } + + if (!isMobileUserAgent && hasSecChUaMobile === '?1') { + suspiciousHeaders.push('desktop-ua-mobile-client-hint') + } + + // Check for browser inconsistencies + if (userAgentLower.includes('chrome') && !hasSecChUa) { + suspiciousHeaders.push('chrome-ua-missing-client-hints') + } + + // Default values for bot detection + let botName = null + let botType = null + + // Check for known bots (existing code) + const checks = { + 'search-engine': KNOWN_SEARCH_BOTS, + 'social': SOCIAL_BOTS, + 'seo': SEO_BOTS, + 'ai': AI_BOTS, + 'generic': GENERIC_BOTS, + 'automation': AUTOMATION_BOTS, + 'http-tool': HTTP_TOOL_BOTS, + 'security-scanner': SECURITY_SCANNING_BOTS, + 'scraping': SCRAPING_BOTS, + 'browser-automation': AUTOMATION_BOTS, + } + + for (const check in checks) { + for (const bot of checks[check as keyof typeof checks]) { + if (userAgentLower.includes(bot.pattern)) { + botName = bot.name + botType = check + break + } + if (bot.secondaryPatterns) { + for (const pattern of bot.secondaryPatterns) { + if (userAgentLower.includes(pattern)) { + botName = bot.name + botType = check + break + } + } + } + } + if (botName) + break + } + + // Bot detected by user agent or suspicious headers + if (botName || suspiciousHeaders.length > 0) { + return { + isBot: true, + data: { + botName: botName || 'suspicious-client', + botType: botType || 'header-anomaly', + suspiciousHeaders: suspiciousHeaders.length > 0 ? suspiciousHeaders : undefined, + }, + } + } + + return { + isBot: false, + } +} diff --git a/src/runtime/server/plugins/botDetection.ts b/src/runtime/server/plugins/botDetection.ts new file mode 100644 index 00000000..cec077fb --- /dev/null +++ b/src/runtime/server/plugins/botDetection.ts @@ -0,0 +1,44 @@ +import { useRuntimeConfig, useStorage } from '#imports' +import { getHeaders, getRequestIP, useSession } from 'h3' +import { defineNitroPlugin } from 'nitropack/runtime' +import { analyzeBotBehavior } from '~/src/runtime/server/lib/is-bot/behavior' +import { isBotFromHeaders } from '~/src/runtime/server/lib/is-bot/userAgent' + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('request', async (event) => { + const ctx = isBotFromHeaders(getHeaders(event)) + event.context.isBot = ctx.isBot + if (!ctx.isBot) { + const kvStorage = useStorage('robots:bot-detection') + const ip = getRequestIP(event) + const path = event.path || '' + const method = event.method || 'GET' + const headers = getHeaders(event) + const config = useRuntimeConfig() + const session = await useSession(event, { + password: config.robots.botDetectionSecret || '80d42cfb-1cd2-462c-8f17-e3237d9027e9', + }) + // Call the extracted core analysis function with data from the event + const behaviorCtx = await analyzeBotBehavior({ + ip, + path, + method, + storage: kvStorage, + session, + headers, + }) + event.context.isBot = behaviorCtx.isLikelyBot + // persist data + event.waitUntil(async () => { + await Promise.all([ + kvStorage.setItem(behaviorCtx.ipKey, behaviorCtx.ipData, { + ttl: 60 * 60 * 24 * 7, // 7 days + }), + kvStorage.setItem(behaviorCtx.sessionKey, behaviorCtx.sessionData, { + ttl: 60 * 60 * 24 * 7, // 7 days + }), + ]) + }) + } + }) +}) diff --git a/src/runtime/server/plugins/botTracker.ts b/src/runtime/server/plugins/botTracker.ts deleted file mode 100644 index 07d75536..00000000 --- a/src/runtime/server/plugins/botTracker.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { defineNitroPlugin } from 'nitropack/runtime' -import type { DailyBotStats } from '#robots/types' - -interface BotVisit { - userAgent: string - path: string - timestamp: number - score: number -} - -// In-memory storage for bot visits -const botStats: DailyBotStats = {} - -// Bot types classification -const KNOWN_SEARCH_BOTS = [ - { pattern: 'googlebot', name: 'googlebot', reputation: 'trusted' }, - { pattern: 'bingbot', name: 'bingbot', reputation: 'trusted' }, - { pattern: 'yandexbot', name: 'yandexbot', reputation: 'trusted' }, - { pattern: 'baiduspider', name: 'baiduspider', reputation: 'trusted' }, - { pattern: 'duckduckbot', name: 'duckduckbot', reputation: 'trusted' }, - { pattern: 'slurp', name: 'yahoo', reputation: 'trusted' }, -] - -const SOCIAL_BOTS = [ - { pattern: 'twitterbot', name: 'twitter', reputation: 'trusted' }, - { pattern: 'facebookexternalhit', name: 'facebook', reputation: 'trusted' }, - { pattern: 'linkedinbot', name: 'linkedin', reputation: 'trusted' }, -] - -const SEO_BOTS = [ - { pattern: 'mj12bot', name: 'majestic12', reputation: 'neutral' }, - { pattern: 'ahrefsbot', name: 'ahrefs', reputation: 'neutral' }, - { pattern: 'semrushbot', name: 'semrush', reputation: 'neutral' }, -] - -const AI_BOTS = [ - { pattern: 'anthropic', name: 'anthropic', reputation: 'neutral' }, - { pattern: 'claude', name: 'claude', reputation: 'neutral' }, - { pattern: 'gptbot', name: 'gpt', reputation: 'neutral' }, - { pattern: 'cohere', name: 'cohere', reputation: 'neutral' }, -] - -const SUSPICIOUS_PATTERNS = [ - { pattern: 'python-requests', name: 'requests', reputation: 'suspicious' }, - { pattern: 'zgrab', name: 'zgrab', reputation: 'malicious' }, - { pattern: 'masscan', name: 'masscan', reputation: 'malicious' }, - { pattern: 'nmap', name: 'nmap', reputation: 'malicious' }, - { pattern: 'nikto', name: 'nikto', reputation: 'malicious' }, - { pattern: 'wpscan', name: 'wpscan', reputation: 'malicious' }, - { pattern: 'wget', name: 'wget', reputation: 'suspicious' }, - { pattern: 'curl', name: 'curl', reputation: 'suspicious' }, -] - -// Helper to identify bots from user agent and score them -function identifyAndScoreBot(userAgent: string, path: string): { botType: string | null, score: number } { - if (!userAgent) return { botType: null, score: 0 } - - const userAgentLower = userAgent.toLowerCase() - let score = 0 - let botType = null - let reputation = 'unknown' - - // Check for known search engines (most trusted, score 90-100) - for (const bot of KNOWN_SEARCH_BOTS) { - if (userAgentLower.includes(bot.pattern)) { - botType = bot.name - reputation = bot.reputation - score = 95 - break - } - } - - // Check for social media bots (trusted, score 80-90) - if (!botType) { - for (const bot of SOCIAL_BOTS) { - if (userAgentLower.includes(bot.pattern)) { - botType = bot.name - reputation = bot.reputation - score = 85 - break - } - } - } - - // Check for SEO tools (neutral, score 50-70) - if (!botType) { - for (const bot of SEO_BOTS) { - if (userAgentLower.includes(bot.pattern)) { - botType = bot.name - reputation = bot.reputation - score = 60 - break - } - } - } - - // Check for AI bots (varies, score 50-70) - if (!botType) { - for (const bot of AI_BOTS) { - if (userAgentLower.includes(bot.pattern)) { - botType = bot.name - reputation = bot.reputation - score = 60 - break - } - } - } - - // Check for suspicious patterns (potentially malicious, score 1-30) - if (!botType) { - for (const pattern of SUSPICIOUS_PATTERNS) { - if (userAgentLower.includes(pattern.pattern)) { - botType = pattern.name - reputation = pattern.reputation - score = reputation === 'malicious' ? 5 : 25 - break - } - } - } - - // Generic bot detection (score varies based on characteristics) - if (!botType && (userAgentLower.includes('bot') || userAgentLower.includes('spider') || userAgentLower.includes('crawler'))) { - botType = 'other-bot' - score = 40 // Default score for unknown bot - - // Adjust score based on heuristics - // Missing Accept headers often indicates a bot - if (!userAgentLower.includes('mozilla')) { - score -= 10 - } - - // Very short user agents are suspicious - if (userAgent.length < 30) { - score -= 10 - } - - // Known bad paths - if (path.includes('wp-login') || path.includes('xmlrpc.php') || path.includes('.env') || path.includes('admin')) { - score -= 15 - } - } - - return { botType, score } -} - -export default defineNitroPlugin((nitroApp) => { - nitroApp.hooks.hook('request', (event) => { - const userAgent = event.node.req.headers['user-agent'] || '' - const path = event.node.req.url || '' - const { botType, score } = identifyAndScoreBot(userAgent, path) - - // Only track bot visits - if (!botType) return - - // Use waitUntil to not block the response - event.waitUntil((async () => { - try { - const now = new Date() - const dateKey = now.toISOString().split('T')[0] // YYYY-MM-DD format - - // Initialize stats for today if not exists - if (!botStats[dateKey]) { - botStats[dateKey] = { - count: 0, - bots: {} - } - } - - // Update stats - botStats[dateKey].count++ - botStats[dateKey].bots[botType] = (botStats[dateKey].bots[botType] || 0) + 1 - - // Optional: you could add a scores object to track average scores per bot type - if (!botStats[dateKey].scores) { - botStats[dateKey].scores = {} - } - - // Track average score per bot type - if (!botStats[dateKey].scores[botType]) { - botStats[dateKey].scores[botType] = { total: 0, count: 0, average: 0 } - } - - botStats[dateKey].scores[botType].total += score - botStats[dateKey].scores[botType].count++ - botStats[dateKey].scores[botType].average = - botStats[dateKey].scores[botType].total / botStats[dateKey].scores[botType].count - - // Expose stats on nitroApp - nitroApp._botStats = botStats - - // Optional: Add this bot score to the response headers for debugging - if (process.env.NODE_ENV === 'development') { - event.node.res.setHeader('X-Bot-Score', score.toString()) - event.node.res.setHeader('X-Bot-Type', botType) - } - } catch (error) { - console.error('Error tracking bot visit:', error) - } - })()) - }) -}) diff --git a/src/runtime/server/routes/__robots__/debug.ts b/src/runtime/server/routes/__robots__/debug.ts index bab69238..7c4cef78 100644 --- a/src/runtime/server/routes/__robots__/debug.ts +++ b/src/runtime/server/routes/__robots__/debug.ts @@ -20,6 +20,5 @@ export default defineEventHandler(async (e) => { env: siteConfig.env, indexable: siteConfig.indexable, }, - botStats: e.context.nitro?.app?._botStats } }) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 053849db..a58c9e7d 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -68,6 +68,9 @@ export interface DailyBotStats { count: number bots: Record scores?: Record + hourly?: number[] // 24 elements array for hourly distribution + paths?: Record> // Bot type -> path -> count + sources?: Record // Detection source -> count } } diff --git a/test/unit/botBehavior.test.ts b/test/unit/botBehavior.test.ts new file mode 100644 index 00000000..bd26f88c --- /dev/null +++ b/test/unit/botBehavior.test.ts @@ -0,0 +1,488 @@ +import type { IPData, SessionData } from '../../src/runtime/server/lib/is-bot/behavior' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + analyzeBotBehavior, + BEHAVIOR_WEIGHTS, + BOT_SCORE_THRESHOLDS, + TrafficType, +} from '../../src/runtime/server/lib/is-bot/behavior' + +describe('bot Detection Analysis', () => { + // Mock storage for testing + const mockStorage = { + getItem: vi.fn(), + setItem: vi.fn().mockResolvedValue(undefined), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + // Helper function to create clean session data + const createCleanSessionData = (overrides = {}): SessionData => ({ + lastRequests: [], + suspiciousPathHits: 0, + maybeSensitivePathHits: 0, + uniqueSensitivePathsAccessed: [], + errorCount: 0, + score: 0, + lastUpdated: Date.now() - 60000, // 1 minute ago + trafficType: TrafficType.UNKNOWN, + knownGoodActions: 0, + requestMethodVariety: [], + requestSequenceEntropy: 0, + firstSeenAt: Date.now() - 60000, // 1 minute ago, + ...overrides, + }) + + // Helper function to create clean IP data + const createCleanIPData = (overrides = {}): IPData => ({ + sessionCount: 1, + activeSessions: ['test-session'], + suspiciousScore: 0, + lastUpdated: Date.now() - 60000, + whitelisted: false, + blacklisted: false, + legitSessionsCount: 0, + lastSessionCreated: Date.now() - 60000, + ...overrides, + }) + + it('should identify a bot accessing a sensitive path', async () => { + // Setup mock storage responses + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData()) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test accessing a sensitive path + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/wp-admin/index.php', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Assertions + expect(result.factors).toHaveProperty('SENSITIVE_PATH') + expect(result.score).toBe(BEHAVIOR_WEIGHTS.SENSITIVE_PATH) + expect(result.isLikelyBot).toBe(false) // Single hit shouldn't trigger "likely bot" + }) + + it('should identify a bot accessing multiple sensitive paths', async () => { + // Mock existing session with previous sensitive path hit + const existingSession = createCleanSessionData({ + suspiciousPathHits: 1, + score: BEHAVIOR_WEIGHTS.SENSITIVE_PATH, + lastRequests: [ + { + timestamp: Date.now() - 30000, + path: '/wp-login', + method: 'GET', + }, + ], + }) + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(existingSession) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test accessing another sensitive path + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/wp-admin/index.php', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Two sensitive path hits should increase score + expect(result.factors).toHaveProperty('SENSITIVE_PATH') + expect(result.score).toBeGreaterThan(BEHAVIOR_WEIGHTS.SENSITIVE_PATH) + // Still not enough to be considered a likely bot + expect(result.isLikelyBot).toBe(false) + }) + + it('should identify a bot accessing maybe-sensitive paths', async () => { + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData()) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test accessing a maybe-sensitive path + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/admin', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should have a lower score than sensitive path + expect(result.factors).toHaveProperty('MAYBE_SENSITIVE_PATH') + expect(result.score).toBe(BEHAVIOR_WEIGHTS.MAYBE_SENSITIVE_PATH) + expect(result.isLikelyBot).toBe(false) + }) + + it('should identify a bot accessing multiple different maybe-sensitive paths', async () => { + // Mock session with a previous maybe-sensitive path hit + const existingSession = createCleanSessionData({ + maybeSensitivePathHits: 1, + uniqueSensitivePathsAccessed: ['/login'], + score: BEHAVIOR_WEIGHTS.MAYBE_SENSITIVE_PATH, + lastRequests: [ + { + timestamp: Date.now() - 30000, + path: '/login', + method: 'GET', + }, + ], + }) + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(existingSession) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test accessing another maybe-sensitive path + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/admin', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should have detected multiple sensitive paths hit + expect(result.factors).toHaveProperty('MAYBE_SENSITIVE_PATH') + expect(result.factors).toHaveProperty('MULTIPLE_SENSITIVE_HITS') + expect(result.score).toBeGreaterThan(BEHAVIOR_WEIGHTS.MAYBE_SENSITIVE_PATH * 2) + expect(result.sensitivePaths).toContain('/login') + expect(result.sensitivePaths).toContain('/admin') + }) + + it('should detect rapid requests', async () => { + // Create session with many recent requests + const now = Date.now() + const recentRequests = Array.from({ length: 20 }, (_, i) => ({ + timestamp: now - (i * 2000), // One request every 2 seconds + path: `/page${i}`, + method: 'GET', + })) + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData({ + lastRequests: recentRequests, + lastUpdated: now - 40000, + })) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test another request coming in + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/another-page', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + timestamp: now, + }) + + // Should detect rapid requests + expect(result.factors).toHaveProperty('RAPID_REQUESTS') + expect(result.isLikelyBot).toBe(false) // Not enough for likely bot yet + }) + + it('should detect abnormal request timing patterns', async () => { + // Create session with suspiciously consistent timing + const now = Date.now() + const consistentRequests = Array.from({ length: 10 }, (_, i) => ({ + timestamp: now - ((i + 1) * 1000), // Exactly 1 second between each request + timeSincePrevious: i > 0 ? 1000 : undefined, // Exactly 1 second + path: `/page${i}`, + method: 'GET', + })) + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData({ + lastRequests: consistentRequests, + lastUpdated: now - 10000, + })) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test another perfectly timed request + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/another-page', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + timestamp: now, + }) + + // Should detect suspicious timing + expect(result.factors).toHaveProperty('RESOURCE_TIMING') + }) + + it('should detect unusual request sequence patterns', async () => { + // Create session with unusual sequential access pattern + const now = Date.now() + // Sequential URLs that look like scanning + const scanningRequests = [ + { timestamp: now - 50000, path: '/page1', method: 'GET' }, + { timestamp: now - 40000, path: '/page2', method: 'GET' }, + { timestamp: now - 30000, path: '/page3', method: 'GET' }, + { timestamp: now - 20000, path: '/page4', method: 'GET' }, + { timestamp: now - 10000, path: '/page5', method: 'GET' }, + ] + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData({ + lastRequests: scanningRequests, + lastUpdated: now - 50000, + })) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test continuing the pattern + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/page6', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + timestamp: now, + }) + + // Should detect unusual pattern + expect(result.factors).toHaveProperty('UNUSUAL_PATTERN') + }) + + it('should respect IP whitelisting', async () => { + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData()) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData({ + whitelisted: true, + })) + } + return Promise.resolve(null) + }) + + // Test a whitelisted IP + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/wp-admin/index.php', // Sensitive path that would normally trigger + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should be whitelisted + expect(result.whitelisted).toBe(true) + expect(result.score).toBe(0) + expect(result.isLikelyBot).toBe(false) + // Should not have called setItem + expect(mockStorage.setItem).not.toHaveBeenCalled() + }) + + it('should respect IP blacklisting', async () => { + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData()) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData({ + blacklisted: true, + })) + } + return Promise.resolve(null) + }) + + // Test a blacklisted IP with innocent path + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/about-us', // Innocent path that would normally not trigger + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should be blacklisted + expect(result.blacklisted).toBe(true) + expect(result.score).toBe(100) + expect(result.isLikelyBot).toBe(true) + // Should not have called setItem + expect(mockStorage.setItem).not.toHaveBeenCalled() + }) + + it('should detect session anomalies', async () => { + // Create IP with many active sessions + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData({ + firstSeenAt: Date.now() - (25 * 60 * 60 * 1000), // 25 hours old + })) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData({ + activeSessions: Array.from({ length: 15 }, (_, i) => `session-${i}`), + sessionCount: 15, + sessionsPerHour: 6, + })) + } + return Promise.resolve(null) + }) + + // Test with a session from this IP + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/some-page', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should detect session anomaly + expect(result.factors).toHaveProperty('SESSION_ANOMALY') + expect(result.sessionAge).toBeGreaterThan(24 * 60 * 60 * 1000) // > 24 hours + }) + + it('should reduce score for natural browsing patterns', async () => { + // Create session with natural browsing pattern + const now = Date.now() + const naturalRequests = [ + { timestamp: now - 50000, path: '/', method: 'GET' }, + { timestamp: now - 42000, path: '/products', method: 'GET' }, + { timestamp: now - 31000, path: '/products/item1', method: 'GET' }, + { timestamp: now - 25000, path: '/products/item2', method: 'GET' }, + { timestamp: now - 10000, path: '/about', method: 'GET' }, + ] + + // Session with some initial score from previous activity + const initialScore = 20 + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(createCleanSessionData({ + lastRequests: naturalRequests, + score: initialScore, + lastUpdated: now - 50000, + })) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData()) + } + return Promise.resolve(null) + }) + + // Test normal browsing continuation + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/contact', + method: 'GET', + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + timestamp: now, + }) + + // Score should be reduced for natural browsing + expect(result.score).toBeLessThan(initialScore) + expect(result.isLikelyBot).toBe(false) + expect(result.legitActions).toBeGreaterThan(0) + }) + + it('should identify a definite bot with high score accumulation', async () => { + // Mock existing session with high suspicious activity + const existingSession = createCleanSessionData({ + suspiciousPathHits: 3, + maybeSensitivePathHits: 2, + uniqueSensitivePathsAccessed: ['/wp-login', '/wp-admin', '/phpmyadmin'], + score: 65, // Close to likely bot threshold + lastRequests: Array.from({ length: 15 }, (_, i) => ({ + timestamp: Date.now() - (i * 2000), + path: i % 2 === 0 ? `/wp-content/page${i}` : `/wp-admin/script${i}`, + method: 'GET', + })), + }) + + mockStorage.getItem.mockImplementation((key: string) => { + if (key === 'session:test-session') { + return Promise.resolve(existingSession) + } + else if (key === 'ip:1.2.3.4') { + return Promise.resolve(createCleanIPData({ + suspiciousScore: 40, + })) + } + return Promise.resolve(null) + }) + + // Hit another sensitive path to push over threshold + const result = await analyzeBotBehavior({ + ip: '1.2.3.4', + path: '/xmlrpc.php', + method: 'POST', // Suspicious POST to xmlrpc + storage: mockStorage, + session: { id: 'test-session' }, + headers: { accept: 'text/html' }, + }) + + // Should now be classified as a likely bot + expect(result.score).toBeGreaterThanOrEqual(BOT_SCORE_THRESHOLDS.LIKELY_BOT) + expect(result.isLikelyBot).toBe(true) + expect(result.trafficType).toBe(TrafficType.MALICIOUS_BOT) + expect(result.factors).toHaveProperty('SENSITIVE_PATH') + }) +}) diff --git a/test/unit/botDetection.test.ts b/test/unit/botDetection.test.ts new file mode 100644 index 00000000..fd811158 --- /dev/null +++ b/test/unit/botDetection.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it } from 'vitest' +import { identifyAndScoreBot } from '../../src/runtime/server/lib/botd' + +const ValidHeaders = { + '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', + 'accept-language': 'en-US,en;q=0.9', + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', +} + +describe('botd detection module', () => { + // Test empty user agent handling + it('handles empty user agent properly', () => { + const result = identifyAndScoreBot('/', {}) + expect(result.botType).toBeNull() + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBe(10) + expect(result.isBotConfidence).toBe(90) + expect(result.source).toBe('empty-ua') + }) + + // Test known search bots + it('identifies Googlebot correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }, + ) + expect(result.botType).toBe('googlebot') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeGreaterThanOrEqual(90) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(80) + expect(result.source).toBe('search-bot') + }) + + it('identifies Bingbot correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' }, + ) + expect(result.botType).toBe('bingbot') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeGreaterThanOrEqual(90) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(80) + expect(result.source).toBe('search-bot') + }) + + // Test social bots + it('identifies Twitter bot correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Twitterbot/1.0' }, + ) + expect(result.botType).toBe('twitter') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeGreaterThanOrEqual(80) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(75) + expect(result.source).toBe('social-bot') + }) + + // Test SEO bots + it('identifies Ahrefs bot correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)' }, + ) + expect(result.botType).toBe('ahrefs') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeGreaterThanOrEqual(60) + expect(result.botReputationScore).toBeLessThanOrEqual(70) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(70) + expect(result.source).toBe('seo-bot') + }) + + // Test AI bots + it('identifies Claude bot correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 (compatible; Claude-Web/1.0; +https://anthropic.com/claude-bot)' }, + ) + expect(result.botType).toBe('anthropic') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeGreaterThanOrEqual(65) + expect(result.botReputationScore).toBeLessThanOrEqual(75) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(75) + expect(result.source).toBe('ai-bot') + }) + + // Test suspicious patterns + it('identifies suspicious Python requests correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'python-requests/2.28.1' }, + ) + expect(result.botType).toBe('requests') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeLessThanOrEqual(30) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(80) + expect(result.source).toBe('suspicious-pattern') + }) + + it('identifies malicious scan tools correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 zgrab/0.x' }, + ) + expect(result.botType).toBe('zgrab') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeLessThanOrEqual(10) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(80) + expect(result.source).toBe('suspicious-pattern') + }) + + // Test generic bot detection + it('identifies generic bots correctly', () => { + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'SomeRandomBot/1.0' }, + ) + expect(result.botType).toBe('other-bot') + expect(result.isBot).toBe(true) + expect(result.botReputationScore).toBeLessThanOrEqual(40) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(65) + expect(result.source).toBe('generic-bot-term') + }) + + // Test path-based scoring adjustments + it('adjusts isBotConfidence for sensitive paths', () => { + const normalResult = identifyAndScoreBot( + '/normal-path', + ValidHeaders, + ) + + const sensitiveResult = identifyAndScoreBot( + '/wp-login', + ValidHeaders, + ) + + expect(sensitiveResult.isBotConfidence).toBeGreaterThan(normalResult.isBotConfidence) + expect(sensitiveResult.source).toBe('sensitive-path') + }) + + // Test human traffic + it('identifies likely human traffic', () => { + const result = identifyAndScoreBot( + '/', + ValidHeaders, + ) + expect(result.botType).toBeNull() + expect(result.isBot).toBe(false) + expect(result.botReputationScore).toBe(0) + expect(result.isBotConfidence).toBe(10) + expect(result.source).toBe('likely-human') + }) + + // Test header analysis + it('uses headers for enhanced detection', () => { + // Missing crucial headers should trigger detection + const result = identifyAndScoreBot( + '/', + { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, + ) + + expect(result.botType).toBe('suspicious-client') + expect(result.isBot).toBe(true) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(70) + expect(result.source).toBe('missing-headers') + }) + + // Test inconsistent user agents + it('detects inconsistent user agents', () => { + const result = identifyAndScoreBot( + '/', + { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Android 10) AppleWebKit/537.36', + }, + ) + + expect(result.botType).toBe('suspicious-client') + expect(result.isBot).toBe(true) + expect(result.isBotConfidence).toBeGreaterThanOrEqual(10) + expect(result.source).toBe('inconsistent-ua') + }) + + // Test isBotConfidence and bot reputation scoring ranges + it('follows proper scoring ranges', () => { + // Test multiple agents to ensure scores stay in proper ranges + const userAgents = [ + 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15', + 'python-requests/2.25.1', + 'curl/7.64.1', + 'nmap/7.80', + 'SomeBotWithVeryLittleInfo', + ] + + for (const ua of userAgents) { + const result = identifyAndScoreBot('/', { 'user-agent': ua }) + + // Bot reputation score should be 0-100 + expect(result.botReputationScore).toBeGreaterThanOrEqual(0) + expect(result.botReputationScore).toBeLessThanOrEqual(100) + + // isBotConfidence should be 0-100 + expect(result.isBotConfidence).toBeGreaterThanOrEqual(0) + expect(result.isBotConfidence).toBeLessThanOrEqual(100) + } + }) +}) diff --git a/test/unit/botTracking.test.ts b/test/unit/botTracking.test.ts new file mode 100644 index 00000000..e045188b --- /dev/null +++ b/test/unit/botTracking.test.ts @@ -0,0 +1,159 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getBotStats, getDayStats, resetStats, trackBotVisit } from '../../src/runtime/server/lib/bot-db' + +describe('bot tracking module', () => { + // Reset stats before each test + beforeEach(() => { + resetStats() + }) + + // Mock Date for consistent testing + const mockDate = new Date('2025-01-04T12:00:00Z') + const realDate = global.Date + + beforeEach(() => { + vi.stubGlobal('Date', class extends realDate { + constructor(...args) { + if (args.length === 0) { + return mockDate + } + return new realDate(...args) + } + }) + }) + + // Restore Date after tests + afterEach(() => { + vi.stubGlobal('Date', realDate) + }) + + it('initializes empty stats properly', () => { + expect(getBotStats()).toEqual({}) + }) + + it('tracks a single bot visit correctly', () => { + trackBotVisit('googlebot', 95, '/', 'search-bot') + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] // '2025-01-04' + + expect(stats[dateKey]).toBeDefined() + expect(stats[dateKey].count).toBe(1) + expect(stats[dateKey].bots.googlebot).toBe(1) + expect(stats[dateKey].scores.googlebot.average).toBe(95) + expect(stats[dateKey].hourly[12]).toBe(1) // 12:00 UTC + expect(stats[dateKey].sources['search-bot']).toBe(1) + }) + + it('tracks multiple bot visits correctly', () => { + // Track different bots + trackBotVisit('googlebot', 95, '/', 'search-bot') + trackBotVisit('bingbot', 96, '/about', 'search-bot') + trackBotVisit('requests', 15, '/wp-login', 'suspicious-pattern') + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] + + expect(stats[dateKey].count).toBe(3) + expect(stats[dateKey].bots.googlebot).toBe(1) + expect(stats[dateKey].bots.bingbot).toBe(1) + expect(stats[dateKey].bots.requests).toBe(1) + + // Check score averages + expect(stats[dateKey].scores.googlebot.average).toBe(95) + expect(stats[dateKey].scores.bingbot.average).toBe(96) + expect(stats[dateKey].scores.requests.average).toBe(15) + + // Check sources + expect(stats[dateKey].sources['search-bot']).toBe(2) + expect(stats[dateKey].sources['suspicious-pattern']).toBe(1) + }) + + it('tracks human traffic correctly', () => { + trackBotVisit(null, 85, '/', 'likely-human') + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] + + expect(stats[dateKey].count).toBe(1) + expect(stats[dateKey].bots.human).toBe(1) + expect(stats[dateKey].scores.human.average).toBe(85) + }) + + it('calculates correct average scores for multiple visits', () => { + // Multiple visits from same bot type with different scores + trackBotVisit('googlebot', 90, '/', 'search-bot') + trackBotVisit('googlebot', 98, '/about', 'search-bot') + trackBotVisit('googlebot', 94, '/contact', 'search-bot') + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] + + expect(stats[dateKey].scores.googlebot.count).toBe(3) + expect(stats[dateKey].scores.googlebot.total).toBe(90 + 98 + 94) + + // Average should be rounded to 2 decimal places + const expectedAvg = Math.round(((90 + 98 + 94) / 3) * 100) / 100 + expect(stats[dateKey].scores.googlebot.average).toBe(expectedAvg) + }) + + it('tracks paths visited', () => { + trackBotVisit('googlebot', 95, '/page1', 'search-bot') + trackBotVisit('googlebot', 96, '/page2', 'search-bot') + trackBotVisit('googlebot', 94, '/page1', 'search-bot') // Repeated path + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] + + expect(stats[dateKey].paths.googlebot['/page1']).toBe(2) + expect(stats[dateKey].paths.googlebot['/page2']).toBe(1) + }) + + it('limits the number of unique paths per bot', () => { + // Track many unique paths to test the limit + const MAX_PATHS = 100 // Same as in implementation + + for (let i = 0; i < MAX_PATHS + 10; i++) { + trackBotVisit('bot', 50, `/path${i}`, 'test') + } + + const stats = getBotStats() + const dateKey = mockDate.toISOString().split('T')[0] + + // Should only track MAX_PATHS unique paths + expect(Object.keys(stats[dateKey].paths.bot).length).toBeLessThanOrEqual(MAX_PATHS) + }) + + it('provides specific day stats', () => { + trackBotVisit('googlebot', 95, '/', 'search-bot') + + const dateKey = mockDate.toISOString().split('T')[0] + const dayStats = getDayStats(dateKey) + + expect(dayStats).toBeDefined() + expect(dayStats.count).toBe(1) + expect(dayStats.bots.googlebot).toBe(1) + + // Test non-existent date + expect(getDayStats('2000-01-01')).toBeNull() + }) + + it('resets stats correctly', () => { + trackBotVisit('googlebot', 95, '/', 'search-bot') + expect(Object.keys(getBotStats()).length).toBeGreaterThan(0) + + resetStats() + expect(Object.keys(getBotStats()).length).toBe(0) + }) + + it('handles errors gracefully', () => { + // Mock console.error to test error handling + const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + // Pass invalid data to trigger error + trackBotVisit('bot', Number.NaN, '/', null) + + expect(mockConsoleError).toHaveBeenCalled() + mockConsoleError.mockRestore() + }) +}) From c83afbb52f0e4cf7b37dcbb8f2abf2c058894f5e Mon Sep 17 00:00:00 2001 From: harlan Date: Thu, 3 Apr 2025 12:51:58 +1100 Subject: [PATCH 03/11] chore: progress --- .playground/pages/index.vue | 8 ++ package.json | 1 + src/module.ts | 6 +- .../app/composables/useBotDetection.ts | 5 + src/runtime/app/plugins/botd.ts | 45 +++++--- src/runtime/server/lib/is-bot/behavior.ts | 86 ++++++++------ src/runtime/server/lib/is-bot/userAgent.ts | 107 +++++++++++------- src/runtime/server/plugins/botDetection.ts | 83 ++++++++------ .../server/routes/__robots__/beacon.ts | 3 + test/unit/botBehavior.test.ts | 26 ++--- 10 files changed, 228 insertions(+), 142 deletions(-) create mode 100644 src/runtime/app/composables/useBotDetection.ts create mode 100644 src/runtime/server/routes/__robots__/beacon.ts diff --git a/.playground/pages/index.vue b/.playground/pages/index.vue index f44fe244..12d1c56f 100644 --- a/.playground/pages/index.vue +++ b/.playground/pages/index.vue @@ -1,9 +1,17 @@ + diff --git a/package.json b/package.json index baa8fbd2..e8fdf80a 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ }, "pnpm": { "onlyBuiltDependencies": [ + "@firebase/util", "@parcel/watcher", "better-sqlite3", "esbuild", diff --git a/src/module.ts b/src/module.ts index 28112104..7ebb6ffe 100644 --- a/src/module.ts +++ b/src/module.ts @@ -531,7 +531,7 @@ declare module 'h3' { handler: resolve('./runtime/server/middleware/injectContext'), }) addServerPlugin(resolve('./runtime/server/plugins/initContext')) - addServerPlugin(resolve('./runtime/server/plugins/botd')) + addServerPlugin(resolve('./runtime/server/plugins/botDetection')) if (isNuxtContentV2) { addServerHandler({ @@ -540,6 +540,10 @@ declare module 'h3' { }) } + addServerHandler({ + route: '/__robots__/beacon', + handler: resolve('./runtime/server/routes/__robots__/beacon'), + }) if (config.debug || nuxt.options.dev) { addServerHandler({ route: '/__robots__/debug.json', diff --git a/src/runtime/app/composables/useBotDetection.ts b/src/runtime/app/composables/useBotDetection.ts new file mode 100644 index 00000000..e28e2235 --- /dev/null +++ b/src/runtime/app/composables/useBotDetection.ts @@ -0,0 +1,5 @@ +import { useState } from 'nuxt/app' + +export function useBotDetection() { + return useState('botContext', () => null) +} diff --git a/src/runtime/app/plugins/botd.ts b/src/runtime/app/plugins/botd.ts index d0b71071..399e80ab 100644 --- a/src/runtime/app/plugins/botd.ts +++ b/src/runtime/app/plugins/botd.ts @@ -1,25 +1,36 @@ -import { defineNuxtPlugin, onNuxtReady, useRequestEvent, useState } from 'nuxt/app' +import { useBotDetection } from '#robots/app/composables/useBotDetection' +import { defineNuxtPlugin, onNuxtReady, useRequestEvent } from 'nuxt/app' export default defineNuxtPlugin({ - async setup(nuxtApp) { - const isBot = useState('isBot', () => null) + async setup() { + const botContextState = useBotDetection() if (import.meta.server) { - isBot.value = useRequestEvent()?.context?.isBot + const context = useRequestEvent()?.context || {} + console.log(context) + botContextState.value = { isBot: context.isBot, ...(context.botContext || {}) } } // need a unique key to store the bot detection result - if (isBot.value) { - await nuxtApp.hooks.callHook('robots:bot-detected') - } - else if (import.meta.client) { - onNuxtReady(async () => { - const { load } = await import('@fingerprintjs/botd') - // Initialize an agent at application startup, once per page/app. - const isBot = (await load()).detect() - if (isBot) { - // persist isBot - await nuxtApp.hooks.callHook('robots:bot-detected') - } - }) + if (import.meta.client) { + const hasVerifiedWithBotD = window.localStorage.getItem('hasVerifiedWithBotD') + const nuxtApp = useNuxtApp() + if (!nuxtApp.payload.serverRendered) { + navigator.sendBeacon('/__robots__/beacon') + } + if (hasVerifiedWithBotD) { + onNuxtReady(async () => { + const { load } = await import('@fingerprintjs/botd').catch(() => ({ load: () => ({}) })) + // Initialize an agent at application startup, once per page/app. + const isBot = (await load() + .catch(() => ({ detect: () => ({ isBot: false }) })) + ).detect() + window.localStorage.getItem('hasVerifiedWithBotD', true) + if (isBot) { + // persist isBot + await nuxtApp.hooks.callHook('robots:bot-detected') + navigator.sendBeacon('/__robots__/beacon?isBot=true') + } + }) + } } }, }) diff --git a/src/runtime/server/lib/is-bot/behavior.ts b/src/runtime/server/lib/is-bot/behavior.ts index d55c66c9..80930165 100644 --- a/src/runtime/server/lib/is-bot/behavior.ts +++ b/src/runtime/server/lib/is-bot/behavior.ts @@ -1,7 +1,7 @@ // src/runtime/server/lib/botd.ts -import type { getHeaders, H3Event } from 'h3' +import type { H3Event } from 'h3' import { useRuntimeConfig } from '#imports' -import { useSession } from 'h3' +import { getHeaders, getQuery, getResponseStatus, useSession } from 'h3' // Common sensitive paths that bots target - expanded with more patterns export const SENSITIVE_PATHS = [ @@ -478,7 +478,33 @@ function detectSessionAnomaly(ipData: IPData, sessionData: SessionData, timestam return result } -export async function analyzeBotBehavior({ +export interface BotAnalysisResult { + score: number + ipScore: number + isLikelyBot: boolean + factors: Record + session: string + ip: string + + // Optional properties based on different execution paths + whitelisted?: boolean + blacklisted?: boolean + skippedProcessing?: boolean + trafficType?: TrafficType + sensitivePaths?: string[] + requestEntropy?: number + legitActions?: number + sessionAge?: number + behaviorChanges?: number + + // For debug/internal use + ipKey?: string + ipData?: IPData + sessionKey?: string + sessionData?: SessionData +} + +export async function analyzeBehavior({ ip, path, method, @@ -494,7 +520,7 @@ export async function analyzeBotBehavior({ session: { id: string } headers: ReturnType timestamp?: number -}) { +}): BotAnalysisResult { // Check if this is an HTML path - if not, skip extensive bot detection const isHtmlPath = isLikelyHtmlPath(path, { accept: headers.accept || '' }) @@ -614,9 +640,6 @@ export async function analyzeBotBehavior({ if (!isHtmlPath && !isMaybeSensitive) { // Still save the request data for context sessionData.lastUpdated = now - // await storage.setItem(sessionKey, sessionData, { - // ttl: 60 * 60 * 24, // 24 hour TTL - // }) // Associate this session with the IP if not already tracked if (!ipData.activeSessions.includes(session.id)) { @@ -635,10 +658,6 @@ export async function analyzeBotBehavior({ } } ipData.lastSessionCreated = now - - // await storage.setItem(ipKey, ipData, { - // ttl: 60 * 60 * 24 * 7, // 7 day TTL for IP data - // }) } // Return current scores without additional processing @@ -843,10 +862,14 @@ export async function analyzeBotBehavior({ // Enhanced bot detection with improved behavior analysis // Update bot score after request completion (to account for status codes) -export async function updateBotScoreAfterRequest(event: H3Event, status: number, storage: any, session, any) { - const sessionData: SessionData | null = await kvStorage.getItem(sessionKey) - if (!sessionData) - return +export async function updateBotScoreAfterRequest( + event: H3Event, + data: BotAnalysisResult, + kvStorage: any, +) { + const sessionData = data.sessionData + const path = event.path + const status = getResponseStatus(event) // Update the last request with the status code if (sessionData.lastRequests.length > 0) { @@ -856,10 +879,6 @@ export async function updateBotScoreAfterRequest(event: H3Event, status: number, // For non-HTML paths, just update the status code without scoring const headers = getHeaders(event) const acceptHeader = headers.accept || '' - if (!isLikelyHtmlPath(path, { accept: acceptHeader }) && !isMaybeSensitivePath(path)) { - await kvStorage.setItem(sessionKey, sessionData) - return - } // Count errors (404s, 403s, etc.) if (status >= 400) { @@ -899,7 +918,7 @@ export async function updateBotScoreAfterRequest(event: H3Event, status: number, || path.includes('/images/') || path.includes('/assets/') - // Don't penalize 404s on common resource paths as much (could be stale cache or changed resource names) + // Don't penalize 404s on common resource paths as much if (!pathIsCommonResource) { sessionData.score += BEHAVIOR_WEIGHTS.NONEXISTENT_RESOURCES } @@ -919,9 +938,6 @@ export async function updateBotScoreAfterRequest(event: H3Event, status: number, else if (sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT) { sessionData.trafficType = TrafficType.SUSPICIOUS } - - // Save updated data - await kvStorage.setItem(sessionKey, sessionData) } else if (status >= 200 && status < 300) { // Successful requests may indicate legitimate use @@ -930,29 +946,31 @@ export async function updateBotScoreAfterRequest(event: H3Event, status: number, // Slightly reduce score for successful HTML page views sessionData.score = Math.max(0, sessionData.score - 1) sessionData.knownGoodActions += 0.5 - await kvStorage.setItem(sessionKey, sessionData) } } // Update IP storage if the score changed significantly if (Math.abs(sessionData.score - (sessionData.lastScore || 0)) > 10) { - const ip = getRequestIP(event) - const ipKey = `ip:${ip}` - const ipData: IPData | null = await kvStorage.getItem(ipKey) - - if (ipData) { + if (data.ipData) { // If this session suddenly became very suspicious, update IP score immediately if (sessionData.score >= BOT_SCORE_THRESHOLDS.LIKELY_BOT && (sessionData.lastScore || 0) < BOT_SCORE_THRESHOLDS.SUSPICIOUS) { - ipData.suspiciousScore = Math.max(ipData.suspiciousScore, sessionData.score * 0.8) - await kvStorage.setItem(ipKey, ipData, { - ttl: 60 * 60 * 24 * 7, // 7 day TTL for IP data - }) + data.ipData.suspiciousScore = Math.max(data.ipData.suspiciousScore, sessionData.score * 0.8) } } // Remember the last score for future comparisons sessionData.lastScore = sessionData.score - await kvStorage.setItem(sessionKey, sessionData) } + + await Promise.all([ + data.ipKey && data.ipData + ? kvStorage.setItem(data.ipKey, data.ipData, { + ttl: 60 * 60 * 24, + }) + : Promise.resolve(), + kvStorage.setItem(data.sessionKey, sessionData, { + ttl: 60 * 60 * 24, + }), + ]) } diff --git a/src/runtime/server/lib/is-bot/userAgent.ts b/src/runtime/server/lib/is-bot/userAgent.ts index fb13db12..c8a256df 100644 --- a/src/runtime/server/lib/is-bot/userAgent.ts +++ b/src/runtime/server/lib/is-bot/userAgent.ts @@ -218,6 +218,54 @@ export const GENERIC_BOTS = [ }, ] +const BotMap = [ + { + type: 'search-engine', + bots: KNOWN_SEARCH_BOTS, + trusted: true, + }, + { + type: 'social', + bots: SOCIAL_BOTS, + trusted: true, + }, + { + type: 'seo', + bots: SEO_BOTS, + trusted: true, + }, + { + type: 'ai', + bots: AI_BOTS, + trusted: true, + }, + { + type: 'generic', + bots: GENERIC_BOTS, + trusted: false, + }, + { + type: 'automation', + bots: AUTOMATION_BOTS, + trusted: false, + }, + { + type: 'http-tool', + bots: HTTP_TOOL_BOTS, + trusted: false, + }, + { + type: 'security-scanner', + bots: SECURITY_SCANNING_BOTS, + trusted: false, + }, + { + type: 'scraping', + bots: SCRAPING_BOTS, + trusted: false, + }, +] + export function isBotFromHeaders(headers: ReturnType): { isBot: boolean data?: { @@ -242,6 +290,23 @@ export function isBotFromHeaders(headers: ReturnType): { } const userAgentLower = userAgent.toLowerCase() + // Check for known bots (existing code) + for (const def of BotMap) { + for (const bot of def.bots) { + for (const pattern of [bot.pattern, ...(bot.secondaryPatterns || [])]) { + if (userAgentLower.includes(pattern)) { + return { + isBot: true, + data: { + botType: def.type, + botName: bot.name, + trusted: def.trusted, + }, + } + } + } + } + } // Check for header inconsistencies const accept = headers.accept @@ -274,53 +339,15 @@ export function isBotFromHeaders(headers: ReturnType): { suspiciousHeaders.push('chrome-ua-missing-client-hints') } - // Default values for bot detection - let botName = null - let botType = null - - // Check for known bots (existing code) - const checks = { - 'search-engine': KNOWN_SEARCH_BOTS, - 'social': SOCIAL_BOTS, - 'seo': SEO_BOTS, - 'ai': AI_BOTS, - 'generic': GENERIC_BOTS, - 'automation': AUTOMATION_BOTS, - 'http-tool': HTTP_TOOL_BOTS, - 'security-scanner': SECURITY_SCANNING_BOTS, - 'scraping': SCRAPING_BOTS, - 'browser-automation': AUTOMATION_BOTS, - } - - for (const check in checks) { - for (const bot of checks[check as keyof typeof checks]) { - if (userAgentLower.includes(bot.pattern)) { - botName = bot.name - botType = check - break - } - if (bot.secondaryPatterns) { - for (const pattern of bot.secondaryPatterns) { - if (userAgentLower.includes(pattern)) { - botName = bot.name - botType = check - break - } - } - } - } - if (botName) - break - } - // Bot detected by user agent or suspicious headers - if (botName || suspiciousHeaders.length > 0) { + if (suspiciousHeaders.length > 0) { return { isBot: true, data: { botName: botName || 'suspicious-client', botType: botType || 'header-anomaly', suspiciousHeaders: suspiciousHeaders.length > 0 ? suspiciousHeaders : undefined, + trusted: false, }, } } diff --git a/src/runtime/server/plugins/botDetection.ts b/src/runtime/server/plugins/botDetection.ts index cec077fb..bf901f1b 100644 --- a/src/runtime/server/plugins/botDetection.ts +++ b/src/runtime/server/plugins/botDetection.ts @@ -1,44 +1,53 @@ -import { useRuntimeConfig, useStorage } from '#imports' -import { getHeaders, getRequestIP, useSession } from 'h3' +import { useStorage } from '#imports' +import {getHeaders, getQuery, getRequestIP, useSession} from 'h3' import { defineNitroPlugin } from 'nitropack/runtime' -import { analyzeBotBehavior } from '~/src/runtime/server/lib/is-bot/behavior' -import { isBotFromHeaders } from '~/src/runtime/server/lib/is-bot/userAgent' +import {analyzeBehavior, TrafficType, updateBotScoreAfterRequest} from '../lib/is-bot/behavior' +import { isBotFromHeaders } from '../lib/is-bot/userAgent' export default defineNitroPlugin((nitroApp) => { + const kvStorage = useStorage('cache:robots:bot-detection') nitroApp.hooks.hook('request', async (event) => { - const ctx = isBotFromHeaders(getHeaders(event)) - event.context.isBot = ctx.isBot - if (!ctx.isBot) { - const kvStorage = useStorage('robots:bot-detection') - const ip = getRequestIP(event) - const path = event.path || '' - const method = event.method || 'GET' - const headers = getHeaders(event) - const config = useRuntimeConfig() - const session = await useSession(event, { - password: config.robots.botDetectionSecret || '80d42cfb-1cd2-462c-8f17-e3237d9027e9', - }) - // Call the extracted core analysis function with data from the event - const behaviorCtx = await analyzeBotBehavior({ - ip, - path, - method, - storage: kvStorage, - session, - headers, - }) - event.context.isBot = behaviorCtx.isLikelyBot - // persist data - event.waitUntil(async () => { - await Promise.all([ - kvStorage.setItem(behaviorCtx.ipKey, behaviorCtx.ipData, { - ttl: 60 * 60 * 24 * 7, // 7 days - }), - kvStorage.setItem(behaviorCtx.sessionKey, behaviorCtx.sessionData, { - ttl: 60 * 60 * 24 * 7, // 7 days - }), - ]) - }) + const ip = getRequestIP(event, { xForwardedFor: true }) + const path = event.path || '' + const method = event.method || 'GET' + const headers = getHeaders(event) + // const config = useRuntimeConfig() + const session = await useSession(event, { + password: '80d42cfb-1cd2-462c-8f17-e3237d9027e9', + }) + const { isBot, data } = isBotFromHeaders(getHeaders(event)) + event.context.isBot = isBot + // we need to watch behavior as well as they may be sending a "trusted" user agent + const behaviorCtx = await analyzeBehavior({ + ip, + path, + method, + storage: kvStorage, + session, + headers, + }) + if (event.path === '/__robots__/beacon' && getQuery(event).isBot) { + // client-side botd.js has detected they are a robot, mark them as a bot + behaviorCtx.sessionData.score = 100 + behaviorCtx.sessionData.trafficType = TrafficType.MALICIOUS_BOT + behaviorCtx.factors['BOTD'] = true + console.log('set ctx', behaviorCtx) + } else { + console.log({ p: event.path, q: getQuery(event).isBot }) + } + event.context.isBot = event.context.isBot || behaviorCtx.isLikelyBot + event.context.botContext = { + ...data, + score: behaviorCtx.score, + trafficType: behaviorCtx.trafficType, + ipScore: behaviorCtx.ipScore, + } + event.context._botBehavior = behaviorCtx + // persist data + }) + nitroApp.hooks.hook('afterResponse', async (event) => { + if (event.context.botBehavior) { + await updateBotScoreAfterRequest(event, event.context._botBehavior, kvStorage) } }) }) diff --git a/src/runtime/server/routes/__robots__/beacon.ts b/src/runtime/server/routes/__robots__/beacon.ts new file mode 100644 index 00000000..89ea9be6 --- /dev/null +++ b/src/runtime/server/routes/__robots__/beacon.ts @@ -0,0 +1,3 @@ +import { defineEventHandler } from 'h3' + +export default defineEventHandler(() => 'OK') diff --git a/test/unit/botBehavior.test.ts b/test/unit/botBehavior.test.ts index bd26f88c..9c65d427 100644 --- a/test/unit/botBehavior.test.ts +++ b/test/unit/botBehavior.test.ts @@ -1,7 +1,7 @@ import type { IPData, SessionData } from '../../src/runtime/server/lib/is-bot/behavior' import { beforeEach, describe, expect, it, vi } from 'vitest' import { - analyzeBotBehavior, + analyzeBehavior, BEHAVIOR_WEIGHTS, BOT_SCORE_THRESHOLDS, TrafficType, @@ -61,7 +61,7 @@ describe('bot Detection Analysis', () => { }) // Test accessing a sensitive path - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/wp-admin/index.php', method: 'GET', @@ -101,7 +101,7 @@ describe('bot Detection Analysis', () => { }) // Test accessing another sensitive path - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/wp-admin/index.php', method: 'GET', @@ -129,7 +129,7 @@ describe('bot Detection Analysis', () => { }) // Test accessing a maybe-sensitive path - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/admin', method: 'GET', @@ -170,7 +170,7 @@ describe('bot Detection Analysis', () => { }) // Test accessing another maybe-sensitive path - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/admin', method: 'GET', @@ -210,7 +210,7 @@ describe('bot Detection Analysis', () => { }) // Test another request coming in - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/another-page', method: 'GET', @@ -249,7 +249,7 @@ describe('bot Detection Analysis', () => { }) // Test another perfectly timed request - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/another-page', method: 'GET', @@ -289,7 +289,7 @@ describe('bot Detection Analysis', () => { }) // Test continuing the pattern - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/page6', method: 'GET', @@ -317,7 +317,7 @@ describe('bot Detection Analysis', () => { }) // Test a whitelisted IP - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/wp-admin/index.php', // Sensitive path that would normally trigger method: 'GET', @@ -348,7 +348,7 @@ describe('bot Detection Analysis', () => { }) // Test a blacklisted IP with innocent path - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/about-us', // Innocent path that would normally not trigger method: 'GET', @@ -384,7 +384,7 @@ describe('bot Detection Analysis', () => { }) // Test with a session from this IP - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/some-page', method: 'GET', @@ -427,7 +427,7 @@ describe('bot Detection Analysis', () => { }) // Test normal browsing continuation - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/contact', method: 'GET', @@ -470,7 +470,7 @@ describe('bot Detection Analysis', () => { }) // Hit another sensitive path to push over threshold - const result = await analyzeBotBehavior({ + const result = await analyzeBehavior({ ip: '1.2.3.4', path: '/xmlrpc.php', method: 'POST', // Suspicious POST to xmlrpc From 8e3981262c67a8a3ebe569f22d295687b41df21d Mon Sep 17 00:00:00 2001 From: harlan Date: Sun, 6 Apr 2025 18:30:45 +1000 Subject: [PATCH 04/11] chore: progress commit --- .playground/nuxt.config.ts | 7 + .playground/pages/index.vue | 3 +- .../3.api/{1.config.md => 0.config.md} | 0 .../{1.nuxt-hooks.md => robots-config.md} | 4 +- src/module.ts | 4 + src/runtime/app/plugins/botd.ts | 24 +- src/runtime/server/lib/is-bot/behavior.ts | 344 ++---------------- src/runtime/server/lib/is-bot/storage.ts | 72 ++++ src/runtime/server/lib/is-bot/userAgent.ts | 9 +- src/runtime/server/plugins/botDetection.ts | 67 ++-- .../server/routes/__robots__/beacon.ts | 6 + src/runtime/server/routes/__robots__/flag.ts | 13 + test/unit/botBehavior.test.ts | 50 +-- test/unit/botDetection.test.ts | 2 +- test/unit/botTracking.test.ts | 159 -------- 15 files changed, 218 insertions(+), 546 deletions(-) rename docs/content/3.api/{1.config.md => 0.config.md} (100%) rename docs/content/3.api/{1.nuxt-hooks.md => robots-config.md} (89%) create mode 100644 src/runtime/server/lib/is-bot/storage.ts create mode 100644 src/runtime/server/routes/__robots__/flag.ts delete mode 100644 test/unit/botTracking.test.ts diff --git a/.playground/nuxt.config.ts b/.playground/nuxt.config.ts index b4e412b2..7586e4b3 100644 --- a/.playground/nuxt.config.ts +++ b/.playground/nuxt.config.ts @@ -14,6 +14,13 @@ export default defineNuxtConfig({ */ defineNuxtModule({ setup(_, nuxt) { + nuxt.hooks.hook('robots:config', (config) => { + const catchAll = config.groups.find(g => g.userAgent.includes('*')) + if (catchAll) { + catchAll.disallow.push('/__link-checker__/') + } + console.log({ catchAll, groups: config.groups }) + }) if (!nuxt.options.dev) return diff --git a/.playground/pages/index.vue b/.playground/pages/index.vue index 12d1c56f..959c1fd5 100644 --- a/.playground/pages/index.vue +++ b/.playground/pages/index.vue @@ -1,8 +1,9 @@ +