diff --git a/.env.example b/.env.example index 3267ed4d..22668142 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,9 @@ MAC_LOOKUP_API_KEY="" IPCHECKING_API_ENDPOINT="" RIPESTAT_SOURCE_APP="" # Security related +# SECURITY_BLACKLIST_LOG_FILE_PATH: opt-in on-disk ledger of rate-limited IPs +# (e.g. "logs/blacklist-ip.log"). Empty = no file; the event is always logged +# via the shared logger either way (and mirrored to Sentry when configured). SECURITY_BLACKLIST_LOG_FILE_PATH="" SECURITY_RATE_LIMIT="" SECURITY_DELAY_AFTER="" @@ -24,6 +27,21 @@ VITE_CURL_IPV6_DOMAIN="" VITE_CURL_IPV64_DOMAIN="" # Google Analytics VITE_GOOGLE_ANALYTICS_ID="" +# Sentry — all optional; leave empty and no Sentry code ships or runs at all. +# VITE_SENTRY_DSN_FRONTEND: frontend DSN (build-time; baked into the bundle; +# also read by the backend at runtime as the allowlist for /api/monitoring, +# the first-party tunnel that relays envelopes past ad blockers) +# SENTRY_DSN_BACKEND: backend DSN (runtime; read by sentry-instrument.js) +# SENTRY_ENVIRONMENT: backend environment tag — unset means "production", +# set "development" on dev machines (frontend tags itself automatically) +# SENTRY_ORG / SENTRY_PROJECT_FRONTEND / SENTRY_AUTH_TOKEN: source-map upload +# at build time only — the token is a secret, never exposed to the browser +VITE_SENTRY_DSN_FRONTEND="" +SENTRY_DSN_BACKEND="" +SENTRY_ENVIRONMENT="" +SENTRY_ORG="" +SENTRY_PROJECT_FRONTEND="" +SENTRY_AUTH_TOKEN="" # MaxMind MAXMIND_ACCOUNT_ID="" MAXMIND_LICENSE_KEY="" diff --git a/api/sentry-tunnel.js b/api/sentry-tunnel.js new file mode 100644 index 00000000..a130ebba --- /dev/null +++ b/api/sentry-tunnel.js @@ -0,0 +1,60 @@ +// /api/monitoring — first-party relay ("tunnel") for frontend Sentry +// envelopes, so ad/privacy blockers that block *.ingest.sentry.io don't +// silence error reporting. The browser SDK posts envelopes here (see +// `tunnel` in frontend/sentry-init.js); we validate that the envelope is +// addressed to OUR project and forward it. backend-server.js mounts the +// route only when VITE_SENTRY_DSN_FRONTEND is set. +import { fetchUpstream } from '../common/fetch-with-timeout.js'; +import logger from '../common/logger.js'; + +// The envelope header is the first newline-delimited JSON line and carries +// the DSN the SDK was configured with. Returns a URL or null. Exported for +// tests. +export const parseEnvelopeDsn = (rawBody) => { + try { + const text = rawBody.toString('utf8'); + const newline = text.indexOf('\n'); + const header = JSON.parse(newline === -1 ? text : text.slice(0, newline)); + return header?.dsn ? new URL(header.dsn) : null; + } catch { + return null; + } +}; + +export default async (req, res) => { + // Defensive method gate (route already POST-only) — covered by tests. + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + // Only the DSN this deployment was built with may be relayed — anything + // else would make us an open proxy to arbitrary Sentry accounts. + const allowedDsn = process.env.VITE_SENTRY_DSN_FRONTEND; + if (!allowedDsn) { + return res.status(404).json({ error: 'Tunnel not configured' }); + } + + const dsn = parseEnvelopeDsn(req.body); + if (!dsn) { + return res.status(400).json({ error: 'Invalid envelope' }); + } + if (dsn.href !== new URL(allowedDsn).href) { + return res.status(403).json({ error: 'DSN not allowed' }); + } + + try { + const projectId = dsn.pathname.replace(/^\//, ''); + const upstream = `https://${dsn.host}/api/${projectId}/envelope/`; + const apiRes = await fetchUpstream(upstream, { + method: 'POST', + headers: { 'Content-Type': 'application/x-sentry-envelope' }, + body: req.body, + }); + res.status(apiRes.status).send(await apiRes.text()); + } catch (error) { + // warn, not error: a transient relay failure is infra noise, not an + // application defect worth a grouped Issue of its own. + logger.warn({ err: error }, 'sentry tunnel forward failed'); + res.status(502).json({ error: 'Relay failed' }); + } +}; diff --git a/backend-server.js b/backend-server.js index cfc53950..94e6524e 100644 --- a/backend-server.js +++ b/backend-server.js @@ -29,6 +29,7 @@ import serviceStatusHandler, { } from './api/service-status.js'; import { getSessionResult as dnsLeakGetResult } from './api/dns-leak-test.js'; import getWhois from './api/get-whois.js'; +import sentryTunnelHandler from './api/sentry-tunnel.js'; import invisibilitytestHandler from './api/invisibility-test.js'; import macChecker from './api/mac-checker.js'; import githubStarsHandler from './api/github-stars.js'; @@ -45,7 +46,12 @@ dotenv.config({ quiet: true }); const app = express(); const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10); -const blackListIPLogFilePath = process.env.SECURITY_BLACKLIST_LOG_FILE_PATH || 'logs/blacklist-ip.log'; +// Local rate-limit ledger file — opt-in: empty means no file is written. +// The logger.warn in the limiter handler always fires regardless (and flows +// to Sentry Logs when a backend DSN is configured), so the file only adds a +// permanent on-disk record for deployments that want one (e.g. Sentry-less +// self-hosts, or feeding a firewall script). +const blackListIPLogFilePath = process.env.SECURITY_BLACKLIST_LOG_FILE_PATH || ''; const rateLimitSet = parseInt(process.env.SECURITY_RATE_LIMIT || 0, 10); const speedLimitSet = parseInt(process.env.SECURITY_DELAY_AFTER || 0, 10); @@ -113,7 +119,6 @@ function logLimitedIP(ip) { if (currentIp === ip) { newCount = parseInt(count, 10) + 1; logExists = true; - logger.warn({ ip, count: newCount }, 'Rate-limited IP hit again'); return `${ip},${newCount},${timestamp}`; } return line; @@ -123,7 +128,6 @@ function logLimitedIP(ip) { if (!logExists) { const newLine = `${ip},${newCount},${formatDate(now)}`; updatedData += (updatedData ? '\n' : '') + newLine; - logger.warn({ ip }, 'IP rate-limited for the first time'); } fs.writeFile(logPath, updatedData, 'utf8', err => { @@ -138,13 +142,22 @@ const rateLimiter = rateLimit({ windowMs: 20 * 60 * 1000, max: rateLimitSet, message: 'Too Many Requests', + // The Sentry tunnel is exempted — it has its own limiter at the route. + // Telemetry sharing the app quota is how reporting silently dies: one + // 429 and the browser SDK drops every event for the next minute. + skip: (req) => req.path === '/monitoring', handler: (req, res, next) => { const ip = getClientIp(req); // Log on the exact transition into rate-limited state — not every // blocked request — to avoid log flooding when an abusive client - // keeps hammering after being limited. - if (req.rateLimit.current === req.rateLimit.limit + 1 && blackListIPLogFilePath) { - logLimitedIP(ip); + // keeps hammering after being limited. The warn line is the primary + // record (mirrored to Sentry Logs when configured); the on-disk + // ledger is the opt-in extra. + if (req.rateLimit.current === req.rateLimit.limit + 1) { + logger.warn({ ip }, 'IP rate-limited'); + if (blackListIPLogFilePath) { + logLimitedIP(ip); + } } res.status(429).json({ message: 'Too Many Requests' }); } @@ -154,6 +167,7 @@ const speedLimiter = slowDown({ windowMs: 60 * 60 * 1000, delayAfter: speedLimitSet, delayMs: (hits) => hits * 400, + skip: (req) => req.path === '/monitoring', }) if (rateLimitSet !== 0) { @@ -231,11 +245,37 @@ app.get('/api/getuserinfo', getUserinfo); app.put('/api/updateuserachievement', updateUserAchievement); app.get('/api/configs', validateConfigs); +// Sentry tunnel — first-party relay for the frontend SDK's envelopes +// Mounted only when this deployment actually built the frontend with a DSN. +// +// `type` must be a catch-all FUNCTION: Replay envelopes are binary +// (deflate-compressed recording) and fetch sends those with NO Content-Type +// header at all — a '*/*' string matcher skips such requests and req.body +// would never be populated. +if (process.env.VITE_SENTRY_DSN_FRONTEND) { + // Dedicated, more generous per-IP limiter + const monitoringLimiter = rateLimit({ + windowMs: 20 * 60 * 1000, + max: 600, + message: 'Too Many Requests', + }); + app.post('/api/monitoring', monitoringLimiter, express.raw({ type: () => true, limit: '10mb' }), sentryTunnelHandler); +} + // Set static file server const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); app.use(express.static(path.join(__dirname, './dist'))); +// Sentry error capture — the SDK itself is initialized (or not) by +// sentry-instrument.js via `node --import`; this attaches the Express error +// handler only when a DSN made that init happen. Must come after every route. +if (process.env.SENTRY_DSN_BACKEND) { + const Sentry = await import('@sentry/node'); + Sentry.setupExpressErrorHandler(app); + logger.info('🛰️ Sentry backend monitoring enabled'); +} + // Bootstrap every offline dataset (MaxMind, CAIDA) before accepting traffic // so we never serve mid-download. Each step is non-fatal: a failure leaves diff --git a/common/caida-updater.js b/common/caida-updater.js index 20c98739..0a7ab641 100644 --- a/common/caida-updater.js +++ b/common/caida-updater.js @@ -17,6 +17,7 @@ import path from 'path'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; import logger from './logger.js'; +import { withCronMonitor } from './sentry-cron.js'; import { createDecompressor } from './decompress.js'; import { AS_ORG_DB_DIR, AS_ORG_FILE, reloadAsOrgDatabase } from './as-org-db.js'; import { AS_REL_DB_DIR, AS_REL_FILE, reloadAsRelDatabase } from './as-rel-db.js'; @@ -96,8 +97,15 @@ export function startCaidaAutoUpdate() { schedulerStarted = true; if (!isAutoUpdateEnabled()) return; + // withCronMonitor reports ok/error check-ins to Sentry Crons (no-op + // without a backend DSN). Note runAllUpdates degrades per-dataset + // internally, so the monitor mainly catches a dead/hung scheduler. const run = () => { - runAllUpdates().catch(error => { + withCronMonitor('caida-auto-update', runAllUpdates, { + schedule: { type: 'interval', value: UPDATE_INTERVAL_MS / (60 * 60 * 1000), unit: 'hour' }, + checkinMargin: 60, + maxRuntime: 30, + }).catch(error => { logger.error({ err: error }, 'CAIDA auto update tick failed'); }); }; diff --git a/common/logger.js b/common/logger.js index d3b201c0..cd839ae6 100644 --- a/common/logger.js +++ b/common/logger.js @@ -3,6 +3,17 @@ // LOG_LEVEL defaults to 'warn'; LOG_FORMAT=json switches off pino-pretty // for log shippers. No NODE_ENV dependency. // +// When SENTRY_DSN_BACKEND is set, the logMethod hook below mirrors log lines +// to Sentry (no official pino transport exists; the hook runs in-process and +// needs no extra dependency), split by severity to match the frontend's +// signal philosophy: +// - warn+ → Sentry Logs (queryable stream, Explore → Logs) +// - error+ → ALSO a Sentry Issue (grouped by the `err` stack when present, +// alertable) — a caught-and-logged upstream failure is a signal, +// not just a log line +// Without the DSN, @sentry/node is never loaded and the hook is not +// installed at all — the usual gating philosophy. +// // ES module imports are hoisted, so backend-server.js's dotenv.config() // runs after this file — we load .env ourselves here so LOG_LEVEL is // honored. dotenv is idempotent; `quiet: true` avoids double banners. @@ -14,8 +25,77 @@ dotenv.config({ quiet: true }); const useJson = process.env.LOG_FORMAT === 'json'; +let sentry = null; +if (process.env.SENTRY_DSN_BACKEND) { + sentry = await import('@sentry/node'); +} + +// Flatten a pino context object into Sentry log attributes, which must be +// primitives: Errors become message + stack strings, nested objects become +// JSON. Exported for tests. +export const flattenLogAttributes = (ctx) => { + const attributes = {}; + for (const [key, value] of Object.entries(ctx)) { + if (value instanceof Error) { + attributes[`${key}.message`] = value.message; + attributes[`${key}.stack`] = value.stack ?? ''; + } else if (typeof value === 'object' && value !== null) { + try { + attributes[key] = JSON.stringify(value); + } catch { + attributes[key] = String(value); + } + } else { + attributes[key] = value; + } + } + return attributes; +}; + +// pino numeric level → Sentry log level; anything below warn stays local. +const SENTRY_LOG_LEVELS = { 40: 'warn', 50: 'error', 60: 'fatal' }; + const logger = pino({ level: process.env.LOG_LEVEL || 'info', + ...(sentry ? { + hooks: { + // Mirror warn+ lines to Sentry Logs and elevate error+ to Issues, + // then log normally. pino call shapes are (msg) or (ctx, msg). + // Only enabled levels reach this hook, so LOG_LEVEL filtering + // applies to Sentry too. + logMethod(args, method, level) { + const sentryLevel = SENTRY_LOG_LEVELS[level]; + if (sentryLevel) { + try { + const [first, second] = args; + const msg = String(typeof first === 'string' ? first : (second ?? '')); + const ctx = (typeof first === 'object' && first !== null) ? first : {}; + const attributes = flattenLogAttributes(ctx); + sentry.logger[sentryLevel](msg, attributes); + if (level >= 50) { + // Group by the original throw site when an err + // object is attached (the logger.error({ err }...) + // convention); the log message rides along as a + // searchable tag. + const captureContext = { + level: sentryLevel, + tags: { log_message: msg }, + extra: attributes, + }; + if (ctx.err instanceof Error) { + sentry.captureException(ctx.err, captureContext); + } else { + sentry.captureMessage(msg, captureContext); + } + } + } catch { + // Telemetry must never break local logging. + } + } + return method.apply(this, args); + }, + }, + } : {}), ...(useJson ? {} : { transport: { target: 'pino-pretty', diff --git a/common/maxmind-updater.js b/common/maxmind-updater.js index 64864d5e..7e7c93d1 100644 --- a/common/maxmind-updater.js +++ b/common/maxmind-updater.js @@ -7,6 +7,7 @@ import { pipeline } from 'stream/promises'; import * as tar from 'tar'; import maxmind from 'maxmind'; import logger from './logger.js'; +import { withCronMonitor } from './sentry-cron.js'; import { getMaxMindDbPaths, MAXMIND_ASN_DB, @@ -69,8 +70,14 @@ export function startMaxMindAutoUpdate({ reload } = {}) { } // Run one update cycle and keep the scheduler alive if that cycle fails. + // withCronMonitor reports ok/error check-ins to Sentry Crons (no-op + // without a backend DSN) so a silently dead scheduler gets noticed. const run = () => { - updateMaxMindDatabases({ reload }).catch(error => { + withCronMonitor('maxmind-auto-update', () => updateMaxMindDatabases({ reload }), { + schedule: { type: 'interval', value: UPDATE_INTERVAL_MS / (60 * 60 * 1000), unit: 'hour' }, + checkinMargin: 60, // minutes of lateness tolerated before "missed" + maxRuntime: 30, // minutes before an in-progress run counts as failed + }).catch(error => { logger.error({ err: error }, 'MaxMind auto update failed'); }); }; diff --git a/common/sentry-cron.js b/common/sentry-cron.js new file mode 100644 index 00000000..23404008 --- /dev/null +++ b/common/sentry-cron.js @@ -0,0 +1,18 @@ +// Sentry Cron check-in wrapper for the backend's periodic jobs (MaxMind / +// CAIDA / service-status). Same gating as sentry-instrument.js: without +// SENTRY_DSN_BACKEND this is a pure passthrough and @sentry/node is never +// loaded, so job modules can call it unconditionally. +// +// The monitor is upserted from the monitorConfig on first check-in — nothing +// to pre-create in the Sentry UI. A rejected job promise reports an "error" +// check-in and rethrows, so callers keep their own .catch logging. + +let sentryWithMonitor = null; +if (process.env.SENTRY_DSN_BACKEND) { + ({ withMonitor: sentryWithMonitor } = await import('@sentry/node')); +} + +export const withCronMonitor = (slug, fn, monitorConfig) => { + if (!sentryWithMonitor) return fn(); + return sentryWithMonitor(slug, fn, monitorConfig); +}; diff --git a/common/service-status-store.js b/common/service-status-store.js index 69e9fa47..c5a6ed03 100644 --- a/common/service-status-store.js +++ b/common/service-status-store.js @@ -11,6 +11,7 @@ import { fetchUpstream } from './fetch-with-timeout.js'; import logger from './logger.js'; +import { withCronMonitor } from './sentry-cron.js'; import { STATUS_PROVIDERS } from './service-status-providers.js'; import { assembleProvider } from './service-status-transform.js'; @@ -106,8 +107,14 @@ export async function bootstrapServiceStatus() { export function startServiceStatusPolling() { if (schedulerStarted) return; schedulerStarted = true; + // withCronMonitor reports ok/error check-ins to Sentry Crons (no-op + // without a backend DSN) so a dead poller surfaces as a missed check-in. const tick = () => { - refreshServiceStatus().catch((error) => { + withCronMonitor('service-status-refresh', refreshServiceStatus, { + schedule: { type: 'interval', value: REFRESH_INTERVAL_MS / (60 * 1000), unit: 'minute' }, + checkinMargin: 5, + maxRuntime: 5, + }).catch((error) => { logger.error({ err: error }, 'service-status refresh tick failed'); }); }; diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 00000000..712ee358 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,22 @@ +// ecosystem.config.cjs — pm2 process definitions for production. +// `pm2 start ecosystem.config.cjs` so nobody has to remember the node flags: +// the backend must load sentry-instrument.js via --import BEFORE express for +// Sentry's ESM auto-instrumentation (a no-op without SENTRY_DSN_BACKEND, so +// Sentry-less deployments can use this file unchanged). +// CommonJS on purpose: pm2 reads its config synchronously and the package is +// "type": "module", hence the .cjs extension. +module.exports = { + apps: [ + { + name: 'myip-backend', + script: 'backend-server.js', + cwd: __dirname, + node_args: '--import ./sentry-instrument.js', + }, + { + name: 'myip-frontend', + script: 'frontend-server.js', + cwd: __dirname, + }, + ], +}; diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index 97aced2a..9511c820 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -34,6 +34,7 @@ import { trackEvent } from '@/utils/analytics'; import { isValidIP } from '@/utils/valid-ip.js'; import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips'; +import { emitAppEvent } from '@/utils/app-events'; import { authenticatedFetch } from '@/utils/authenticated-fetch'; import IPCard from './ip-infos/IPCard.vue'; @@ -136,6 +137,18 @@ const markFetched = (cardID) => { trackFetchStatus(fetchStatus); }; +// Stable source slugs by card ID, used in the ip-source:exhausted domain +// event (function names would be mangled by minification). Order matches the +// ipSources table in checkAllIPs. +const CARD_SOURCE_SLUGS = [ + 'ipchecking-v4', + 'ipchecking-v6', + 'cloudflare-v4', + 'cloudflare-v6', + 'ipip', + 'ipchecking-64', +]; + // Phase 1 — resolve one card's IP and render it immediately. const resolveIP = async (cardID, getFromSource) => { let ip = null; @@ -153,9 +166,17 @@ const resolveIP = async (cardID, getFromSource) => { IPArray.value = [...IPArray.value, { ip, country: '' }]; } else { // v6 cards: ipchecking_v6 (1), cloudflare_v6 (3). - ipDataCards[cardID].ip = (cardID === 1 || cardID === 3) + const isV6Card = cardID === 1 || cardID === 3; + ipDataCards[cardID].ip = isV6Card ? t('ipInfos.IPv6Error') : t('ipInfos.IPv4Error'); + // Domain event: this source AND all its internal fallbacks failed to + // produce an IP. Emitted unconditionally (bus semantics); subscribers + // decide what matters — e.g. sentry-init.js reports v4 exhaustion. + emitAppEvent('ip-source:exhausted', { + source: CARD_SOURCE_SLUGS[cardID] ?? `card-${cardID}`, + ipVersion: isV6Card ? 'v6' : 'v4', + }); markFetched(cardID); // no IP → no detail phase } return { cardID, ip }; diff --git a/frontend/main.js b/frontend/main.js index 0f2a9f66..d530d901 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -51,6 +51,19 @@ const store = useMainStore(pinia); app.use(i18n); app.use(router); +// Sentry error monitoring — VITE_SENTRY_DSN_FRONTEND is a build-time +// constant, so when it's unset Vite folds this branch away and the Sentry +// chunk neither ships nor loads. The returned promise joins the mount gate +// below: the chunk loads in parallel with the auth/config/locale fetches the +// mount already waits for, so console/error instrumentation is guaranteed to +// be in place before any component code runs (checkAllIPs fires immediately +// at mount). A failed load must never block the app — hence the catch. +const sentryReady = import.meta.env.VITE_SENTRY_DSN_FRONTEND + ? import('./sentry-init') + .then(({ initSentry }) => initSentry(app, router)) + .catch(() => {}) + : Promise.resolve(); + // // Initialize a series of operations // @@ -91,7 +104,8 @@ Promise.all([ store.isFireBaseSet ? store.initializeAuthListener() : Promise.resolve(), store.loadPreferences(), // Load user preferences store.fetchConfigs(), // Fetch backend configs - loadActiveLocaleMessages() // Load the active language pack before first render + loadActiveLocaleMessages(), // Load the active language pack before first render + sentryReady // Sentry instrumentation in place before first render ]).then(() => { app.mount('#app'); }).catch(error => { diff --git a/frontend/sentry-init.js b/frontend/sentry-init.js new file mode 100644 index 00000000..0aeceb6c --- /dev/null +++ b/frontend/sentry-init.js @@ -0,0 +1,73 @@ +// Sentry error monitoring for the SPA. This module is loaded on demand by +// main.js ONLY when VITE_SENTRY_DSN_FRONTEND is set at build time — a build +// without the env var never ships nor requests this chunk, so self-hosted +// deployments stay completely Sentry-free. +// +// App code must never import Sentry directly (it would drag the SDK back into +// the main bundle and bypass the gating). For explicit signals, emit a domain +// event on utils/app-events.js and subscribe to it here instead. +import * as Sentry from '@sentry/vue'; +import { onAppEvent } from '@/utils/app-events'; + +const env = import.meta.env ?? {}; + +const initSentry = (app, router) => { + Sentry.init({ + app, + dsn: env.VITE_SENTRY_DSN_FRONTEND, + // Distinguish local `pnpm dev` events from production ones in the + // Sentry UI ("development" / "production") + environment: env.MODE, + integrations: [ + // Route-change performance tracing via vue-router → Web Vitals + // (LCP / CLS / INP) and per-route load metrics under Insights + Sentry.browserTracingIntegration({ router }), + // Session Replay, error-only: nothing is uploaded unless an error event fires + Sentry.replayIntegration(), + // console.error() → grouped Issues (tagged logger:console). + Sentry.captureConsoleIntegration({ levels: ['error'] }), + ], + // 100% trace sampling — Should change based on plan quota + tracesSampleRate: 1.0, + // Replay: never record plain sessions, always keep the buffer that + // gets flushed when an error occurs + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 1.0, + // MyIP is a privacy tool: never attach user IP / headers to events + sendDefaultPii: false, + // Relay envelopes through our own backend (api/sentry-tunnel.js) + tunnel: '/api/monitoring', + // Console-captured events group by the console message instead of + // the exception stack. The fallback chains all surface the same + // "TypeError: Failed to fetch" from fetchWithTimeout, so stack + // grouping would collapse every source's degradation into ONE issue + // — indistinguishable per source, and a single archive/discard on it + // silently hides them all. + beforeSend(event) { + if (event.logger === 'console') { + const firstArg = event.extra?.arguments?.[0]; + if (typeof firstArg === 'string' && firstArg.trim()) { + event.fingerprint = [firstArg.trim().slice(0, 200)]; + } + } + return event; + }, + }); + + // Explicit product signals arrive via the app-events bus — business code + // stays Sentry-free and just emits domain events (see frontend/AGENTS.md). + + // A whole IP source chain (primary + every fallback) failed. v6 + // exhaustion is routine for IPv4-only visitors, so only v4 is reported; + // per-hop degradation inside a chain is already covered by the + // console.error capture above. + onAppEvent('ip-source:exhausted', ({ source, ipVersion }) => { + if (ipVersion !== 'v4') return; + Sentry.captureMessage(`IP source chain exhausted: ${source}`, { + level: 'error', + tags: { source, ipVersion }, + }); + }); +}; + +export { initSentry }; diff --git a/package.json b/package.json index c51c4cf4..49bb9c09 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,14 @@ "type": "module", "packageManager": "pnpm@11.10.0", "scripts": { - "dev": "concurrently \"vite\" \"nodemon backend-server.js\"", + "dev": "concurrently \"vite\" \"nodemon --import ./sentry-instrument.js backend-server.js\"", "build": "vite build", "test": "node --test tests/*.test.js", "check": "pnpm run test && pnpm run build", "preview": "vite preview", - "start-backend": "node backend-server.js", + "start-backend": "node --import ./sentry-instrument.js backend-server.js", "start-frontend": "node frontend-server.js", - "start": "concurrently \"node frontend-server.js\" \"node backend-server.js\"" + "start": "concurrently \"node frontend-server.js\" \"node --import ./sentry-instrument.js backend-server.js\"" }, "dependencies": { "@cloudflare/speedtest": "^1.11.0", @@ -20,6 +20,8 @@ "@iconify/vue": "^5.0.1", "@khmyznikov/pwa-install": "^0.6.4", "@lucide/vue": "^1.23.0", + "@sentry/node": "^10.64.0", + "@sentry/vue": "^10.64.0", "@tanstack/vue-table": "^8.21.3", "@thumbmarkjs/thumbmarkjs": "^1.10.0", "@vueuse/core": "^14.3.0", @@ -34,7 +36,7 @@ "express": "^5.2.1", "express-rate-limit": "^8.5.2", "express-slow-down": "^3.1.0", - "firebase": "^12.15.0", + "firebase": "^12.16.0", "html-to-image": "^1.11.13", "http-proxy-middleware": "^4.2.0", "maxmind": "^5.0.6", @@ -58,12 +60,13 @@ "whoiser": "^1.18.0" }, "devDependencies": { + "@sentry/vite-plugin": "^5.3.0", "@tailwindcss/vite": "^4.3.2", "@vitejs/plugin-vue": "^6.0.7", "code-inspector-plugin": "^1.6.6", "nodemon": "^3.1.14", "tailwindcss": "^4.3.2", "vconsole": "^3.15.1", - "vite": "^8.1.3" + "vite": "^8.1.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03a8a1f4..15a6c14d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@lucide/vue': specifier: ^1.23.0 version: 1.23.0(vue@3.5.39) + '@sentry/node': + specifier: ^10.64.0 + version: 10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)) + '@sentry/vue': + specifier: ^10.64.0 + version: 10.64.0(pinia@3.0.4(vue@3.5.39))(vue@3.5.39) '@tanstack/vue-table': specifier: ^8.21.3 version: 8.21.3(vue@3.5.39) @@ -66,8 +72,8 @@ importers: specifier: ^3.1.0 version: 3.1.0(express@5.2.1(supports-color@5.5.0)) firebase: - specifier: ^12.15.0 - version: 12.15.0 + specifier: ^12.16.0 + version: 12.16.0 html-to-image: specifier: ^1.11.13 version: 1.11.13 @@ -124,7 +130,7 @@ importers: version: 2.3.1(vue@3.5.39) vue-router: specifier: ^5.1.0 - version: 5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(vue@3.5.39))(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39) + version: 5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(vue@3.5.39))(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39) vue-sonner: specifier: ^2.0.9 version: 2.0.9 @@ -132,12 +138,15 @@ importers: specifier: ^1.18.0 version: 1.18.0 devDependencies: + '@sentry/vite-plugin': + specifier: ^5.3.0 + version: 5.3.0 '@tailwindcss/vite': specifier: ^4.3.2 - version: 4.3.2(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.2(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39) + version: 6.0.7(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39) code-inspector-plugin: specifier: ^1.6.6 version: 1.6.6(supports-color@5.5.0) @@ -151,15 +160,60 @@ importers: specifier: ^3.15.1 version: 3.15.1 vite: - specifier: ^8.1.3 - version: 8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + specifier: ^8.1.4 + version: 8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) packages: + '@apm-js-collab/code-transformer-bundler-plugins@0.5.0': + resolution: {integrity: sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ==} + engines: {node: '>=18.0.0'} + + '@apm-js-collab/code-transformer@0.15.0': + resolution: {integrity: sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww==} + hasBin: true + + '@apm-js-collab/tracing-hooks@0.10.1': + resolution: {integrity: sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0-rc.6': resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==} engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -184,6 +238,14 @@ packages: resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -203,6 +265,14 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -284,15 +354,15 @@ packages: peerDependencies: '@firebase/app': 0.x - '@firebase/app-compat@0.5.14': - resolution: {integrity: sha512-rgFmiofYsdS9ZG/Bht3OBxJtPD3zWE1cffShWubEm+4+qZeyzCbmtb1q6jOEjN9fB7uufe4rQmWOPXouR3758Q==} + '@firebase/app-compat@0.5.15': + resolution: {integrity: sha512-HaiSM9TwbGIR4b7F6+UncHWlqdH89eeY7VUskaOGOlI2PxHS5Z+6hHsYGvNLy0SHDE6zyXO+3QSA6a4aqQxsqA==} engines: {node: '>=20.0.0'} '@firebase/app-types@0.9.5': resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} - '@firebase/app@0.15.0': - resolution: {integrity: sha512-soIskolmGgbpi0K/MfrjtdpO1220qRCbXA4Z8Qx3lM+fVwA3q40m+OM+7zBHd2nuQCrLXb33L6Oc1aBH3Y26AQ==} + '@firebase/app@0.15.1': + resolution: {integrity: sha512-iD9+Z5HcPo0Uop5f72/VYMeXwKucBhW7iFrISkJFvQ+lSZikTNgTz0FgAtaaTkAG0pEZSnCymA2Fu49n0rcufQ==} engines: {node: '>=20.0.0'} '@firebase/auth-compat@0.6.8': @@ -418,16 +488,16 @@ packages: peerDependencies: '@firebase/app': 0.x - '@firebase/remote-config-compat@0.2.26': - resolution: {integrity: sha512-uC57Tc7GYYOCnMgLkGIVf999XlaYaPDONoa54c93YTKDctlvCZI89z0zQ2RbhGR8Zf+QuCbQHs/99vqoE84a7g==} + '@firebase/remote-config-compat@0.2.27': + resolution: {integrity: sha512-FYwYWwSbUdza/pRX4NpSBm/Pimntum3jEIBpnDn5Ey1jHNWgjxrE8Z5SB4mCHd5wGCoYd3koJzxARl/VWIEx0Q==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/remote-config-types@0.5.1': resolution: {integrity: sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==} - '@firebase/remote-config@0.8.5': - resolution: {integrity: sha512-zb+7CDGFP2wYVF1LXQoYIFdoESIQM3p0+uiW1welw8+zvDxAL50K75PKTXXtunJADUrksTVpV7mD0pn54vzJRA==} + '@firebase/remote-config@0.9.0': + resolution: {integrity: sha512-aNn6/eJhsSC+gXSToiXiYPv3ypLP9lFtzl+/q9kSOBPB7D6rae0Rt2uENZZLXGYbEgHYKQblOhijJAXGbbJjtQ==} peerDependencies: '@firebase/app': 0.x @@ -563,6 +633,48 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.220.0': + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.9.0': + resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@oxc-project/types@0.138.0': resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} @@ -697,6 +809,157 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sentry/babel-plugin-component-annotate@5.3.0': + resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==} + engines: {node: '>= 18'} + + '@sentry/browser-utils@10.64.0': + resolution: {integrity: sha512-qXvUpsZdE0+6SovhDZvjN4JkqQEpzeYsrOF5g63wMsqJWWv2vW/pQZIlJs0F6C0t4q5AHZL4fl5rNkvpVqFdKA==} + engines: {node: '>=18'} + + '@sentry/browser@10.64.0': + resolution: {integrity: sha512-CHZ7+79evh8knBtVHjW/XqV3mRaWs2TdjX7i55zpFe9vcNngQHV++0ss8ird/xfMY1mfCDtbyAN+Z6XY2o5aJA==} + engines: {node: '>=18'} + + '@sentry/bundler-plugin-core@5.3.0': + resolution: {integrity: sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==} + engines: {node: '>= 18'} + + '@sentry/cli-darwin@2.58.6': + resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.58.6': + resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd, android] + + '@sentry/cli-linux-arm@2.58.6': + resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd, android] + + '@sentry/cli-linux-i686@2.58.6': + resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd, android] + + '@sentry/cli-linux-x64@2.58.6': + resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd, android] + + '@sentry/cli-win32-arm64@2.58.6': + resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@sentry/cli-win32-i686@2.58.6': + resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.58.6': + resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.58.6': + resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/conventions@0.15.1': + resolution: {integrity: sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==} + engines: {node: '>=14'} + + '@sentry/core@10.64.0': + resolution: {integrity: sha512-HjojJcXD1l2qZ1AXje2s0XY/nYsaUt00wsM1HMBImA8vAClyPisFE/CC0/UD6pEvsGFhVgi8Dcxo7EN41uyeFw==} + engines: {node: '>=18'} + + '@sentry/feedback@10.64.0': + resolution: {integrity: sha512-YrmKwRoAto+nwo26DoAhnpNmhkrVkuQFIAXqlTD8ipERjhcSNUYJAkAB+nH4w1KIgm9QQZE1sTq3iNQdPl1XMQ==} + engines: {node: '>=18'} + + '@sentry/node-core@10.64.0': + resolution: {integrity: sha512-dcma5uEWl0SXywY4vzrlOwuz4NBPyPhrzqL1NjlU6Di/OVbyYAZJPCwsR8XYDz73PGc5sU3qKSRoXWX56Ip0wA==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + + '@sentry/node@10.64.0': + resolution: {integrity: sha512-rhNZ3CTqwdTQHo9Zd+NsoLyW69VIzbMcGMRX9y8W1A6runT4YH/MDhmT0U9oWfNZKN8ngqR/gfVMTnlYVQliCA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.64.0': + resolution: {integrity: sha512-hXw8dwSSA9/9r3VGy0PO2SJp7g5/yH2+7Bak60EkOT21AT1BwFnVEsVQyZb+UHjG7UWne/jDbwdWPSUm/rtovw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + + '@sentry/replay-canvas@10.64.0': + resolution: {integrity: sha512-EioBwcnnhtPiXzTZJTbZKaMEg3qNM5YZlX1VanoXomPATqfJD62cb/M27zhgiWXXJ7e8/NGVHvwNDYGrqsN39g==} + engines: {node: '>=18'} + + '@sentry/replay@10.64.0': + resolution: {integrity: sha512-q8nke2GDSwEiu1zYoctDDvnSNTjHWoVAKo2JXTleD0nwOO6IlAgUYmsm3qEQiSW7BVla9W1TRbW6ChFAUXYZbw==} + engines: {node: '>=18'} + + '@sentry/rollup-plugin@5.3.0': + resolution: {integrity: sha512-hgPGPYdQJ/G1cGYOxAb7d4z3V+/k/E5/P/5TFPEEBLuIbFFk+JG0CISUDJdzXJjO382Lb99PBJuXGbueBmO79w==} + engines: {node: '>= 18'} + peerDependencies: + rollup: '>=3.2.0' + peerDependenciesMeta: + rollup: + optional: true + + '@sentry/server-utils@10.64.0': + resolution: {integrity: sha512-d568Jhn3WBfm07dsdPsLh0q+qRj71xZEDZFsA33kizD0UuSCR8axtc2YW6aKdPhtuqgm9tHjSX35Q9vK/XmujA==} + engines: {node: '>=18'} + + '@sentry/vite-plugin@5.3.0': + resolution: {integrity: sha512-qcoSzo4n2MulVQ70UUPLq6dTleb2a2HwL2wuwvAgWhPChrYTuk6A6mDg6aQb9fairPAwFPiU9PzOANpoDJcz1A==} + engines: {node: '>= 18'} + + '@sentry/vue@10.64.0': + resolution: {integrity: sha512-eh+S4dmdfX495KQOwy9j4p4lyP/fFyXIMGyuZuIvjM2H2NQoaGjh40Xn/Np32T0l9YXsPsWljm6tEMr/n10Aug==} + engines: {node: '>=18'} + peerDependencies: + '@tanstack/vue-router': ^1.64.0 + pinia: 2.x || 3.x + vue: 2.x || 3.x + peerDependenciesMeta: + '@tanstack/vue-router': + optional: true + pinia: + optional: true + '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} @@ -821,6 +1084,9 @@ packages: '@types/dom-chromium-installation-events@101.0.4': resolution: {integrity: sha512-jV4HXmW5D18bpndzAPF1REGE5xagQqrAexHgX9WoWIPpNaaHtsHQgu5uFbL2z0NIc9fOD0TIC1TRpJhYA1OPxw==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} @@ -954,6 +1220,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -989,6 +1259,10 @@ packages: resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==} engines: {node: '>=20.19.0'} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1003,6 +1277,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1022,6 +1301,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.5: + resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1037,6 +1321,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001803: + resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + chalk@4.1.1: resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} engines: {node: '>=10'} @@ -1068,6 +1355,9 @@ packages: circle-progress.vue@3.4.1: resolution: {integrity: sha512-JO3YrBFYx/EE29LHtM4u1j3YaIOyOfNIYh2pUfdZ0nf+wMFAH+HYcG3YH8HfBLnBzypnIOk+37NihPXKX7MoWQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1115,6 +1405,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1184,6 +1477,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1217,6 +1513,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1228,6 +1527,14 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1281,8 +1588,12 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} - firebase@12.15.0: - resolution: {integrity: sha512-p0YTLcRSTiBXMx9sGr4ZNSfLjc/RVBEw4C/TXjVMtw65+6E1Pbm47UY3F4/AqRoDobEcNX3gsbPGy7jPjxbgSQ==} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + firebase@12.16.0: + resolution: {integrity: sha512-CNw6hFBdONkzF8UGLDx/RDRY9gVa5VmJNHd7qi4gdmA3ZuLkuOrhmWefB2l+FN+OxFpN77Itq7aO6zlTi780ag==} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1300,6 +1611,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1320,6 +1635,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1366,6 +1685,10 @@ packages: resolution: {integrity: sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==} engines: {node: ^22.15.0 || ^24.0.0 || >=26.0.0} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + httpxy@0.5.4: resolution: {integrity: sha512-URfeibL0kTH6VuIxxaJDXWQWEk8fKr+9L8MGv6CuAiNy0fGnoVhWbXBvJR1mkdsvCDUxvhX9cW60k2AhtH5s6w==} @@ -1382,6 +1705,10 @@ packages: ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + import-in-the-middle@3.3.1: + resolution: {integrity: sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==} + engines: {node: '>=18'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1427,6 +1754,9 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1435,6 +1765,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1538,6 +1871,10 @@ packages: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -1547,6 +1884,13 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string-ast@1.0.3: resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} engines: {node: '>=20.19.0'} @@ -1577,6 +1921,10 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + meriyah@6.1.4: + resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} + engines: {node: '>=18.0.0'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1617,6 +1965,9 @@ packages: resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==} engines: {node: '>=10', npm: '>=6'} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1635,6 +1986,19 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + nodemon@3.1.14: resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} engines: {node: '>=10'} @@ -1662,10 +2026,26 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -1689,6 +2069,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pinia@3.0.4: resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} peerDependencies: @@ -1725,10 +2109,6 @@ packages: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.16: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} @@ -1736,6 +2116,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + protobufjs@7.5.8: resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} engines: {node: '>=12.0.0'} @@ -1744,6 +2128,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -1800,6 +2187,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -1831,6 +2222,13 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -1878,6 +2276,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -1977,6 +2379,9 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2025,6 +2430,12 @@ packages: resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2038,8 +2449,8 @@ packages: vconsole@3.15.1: resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} - vite@8.1.3: - resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2146,6 +2557,9 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2157,6 +2571,14 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + whoiser@1.18.0: resolution: {integrity: sha512-QRIGreBuouc8d9i+UVMFqYJSiG7gaoaGX8nKugYDGqnuNLLgjDBwmlKODOIGHveBawza3Kfkk/OuM9VsTUYwaA==} engines: {node: '>=15.0.0'} @@ -2176,6 +2598,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2201,8 +2626,72 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: + '@apm-js-collab/code-transformer-bundler-plugins@0.5.0': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + es-module-lexer: 2.3.0 + magic-string: 0.30.21 + module-details-from-path: 1.0.4 + + '@apm-js-collab/code-transformer@0.15.0': + dependencies: + '@types/estree': 1.0.9 + astring: 1.9.0 + esquery: 1.7.0 + meriyah: 6.1.4 + semifies: 1.0.0 + source-map: 0.6.1 + + '@apm-js-collab/tracing-hooks@0.10.1': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + debug: 4.4.3(supports-color@5.5.0) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7(supports-color@5.5.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@5.5.0) + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/generator@8.0.0-rc.6': dependencies: '@babel/parser': 8.0.0-rc.6 @@ -2212,6 +2701,32 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7(supports-color@5.5.0) + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7(supports-color@5.5.0) + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.29.7': {} @@ -2224,6 +2739,13 @@ snapshots: '@babel/helper-validator-identifier@8.0.0-rc.6': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -2238,6 +2760,24 @@ snapshots: '@babel/runtime@7.29.7': {} + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7(supports-color@5.5.0)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2313,9 +2853,9 @@ snapshots: tslib: 2.8.1 optional: true - '@firebase/ai@2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + '@firebase/ai@2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/app-check-interop-types': 0.3.4 '@firebase/app-types': 0.9.5 '@firebase/component': 0.7.3 @@ -2323,11 +2863,11 @@ snapshots: '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/analytics-compat@0.2.28(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/analytics-compat@0.2.28(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/analytics': 0.10.22(@firebase/app@0.15.0) + '@firebase/analytics': 0.10.22(@firebase/app@0.15.1) '@firebase/analytics-types': 0.8.4 - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2336,20 +2876,20 @@ snapshots: '@firebase/analytics-types@0.8.4': {} - '@firebase/analytics@0.10.22(@firebase/app@0.15.0)': + '@firebase/analytics@0.10.22(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/app-check-compat@0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/app-check-compat@0.4.5(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-check': 0.12.0(@firebase/app@0.15.0) + '@firebase/app-check': 0.12.0(@firebase/app@0.15.1) '@firebase/app-check-types': 0.5.4 - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2361,17 +2901,17 @@ snapshots: '@firebase/app-check-types@0.5.4': {} - '@firebase/app-check@0.12.0(@firebase/app@0.15.0)': + '@firebase/app-check@0.12.0(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/app-compat@0.5.14': + '@firebase/app-compat@0.5.15': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2381,7 +2921,7 @@ snapshots: dependencies: '@firebase/logger': 0.5.1 - '@firebase/app@0.15.0': + '@firebase/app@0.15.1': dependencies: '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 @@ -2389,10 +2929,10 @@ snapshots: idb: 7.1.1 tslib: 2.8.1 - '@firebase/auth-compat@0.6.8(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + '@firebase/auth-compat@0.6.8(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 - '@firebase/auth': 1.13.3(@firebase/app@0.15.0) + '@firebase/app-compat': 0.5.15 + '@firebase/auth': 1.13.3(@firebase/app@0.15.1) '@firebase/auth-types': 0.13.1(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 @@ -2409,9 +2949,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/auth@1.13.3(@firebase/app@0.15.0)': + '@firebase/auth@1.13.3(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2422,9 +2962,9 @@ snapshots: '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/data-connect@0.7.1(@firebase/app@0.15.0)': + '@firebase/data-connect@0.7.1(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/auth-interop-types': 0.2.5 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 @@ -2455,11 +2995,11 @@ snapshots: faye-websocket: 0.11.4 tslib: 2.8.1 - '@firebase/firestore-compat@0.4.11(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + '@firebase/firestore-compat@0.4.11(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 - '@firebase/firestore': 4.16.0(@firebase/app@0.15.0) + '@firebase/firestore': 4.16.0(@firebase/app@0.15.1) '@firebase/firestore-types': 3.0.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2472,9 +3012,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/firestore@4.16.0(@firebase/app@0.15.0)': + '@firebase/firestore@4.16.0(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 @@ -2484,11 +3024,11 @@ snapshots: re2js: 0.4.3 tslib: 2.8.1 - '@firebase/functions-compat@0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/functions-compat@0.4.5(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 - '@firebase/functions': 0.13.5(@firebase/app@0.15.0) + '@firebase/functions': 0.13.5(@firebase/app@0.15.1) '@firebase/functions-types': 0.6.4 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2497,9 +3037,9 @@ snapshots: '@firebase/functions-types@0.6.4': {} - '@firebase/functions@0.13.5(@firebase/app@0.15.0)': + '@firebase/functions@0.13.5(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/app-check-interop-types': 0.3.4 '@firebase/auth-interop-types': 0.2.5 '@firebase/component': 0.7.3 @@ -2507,11 +3047,11 @@ snapshots: '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/installations-compat@0.2.22(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + '@firebase/installations-compat@0.2.22(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) '@firebase/installations-types': 0.5.4(@firebase/app-types@0.9.5) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2523,9 +3063,9 @@ snapshots: dependencies: '@firebase/app-types': 0.9.5 - '@firebase/installations@0.6.22(@firebase/app@0.15.0)': + '@firebase/installations@0.6.22(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 idb: 7.1.1 @@ -2535,11 +3075,11 @@ snapshots: dependencies: tslib: 2.8.1 - '@firebase/messaging-compat@0.2.27(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/messaging-compat@0.2.27(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 - '@firebase/messaging': 0.13.0(@firebase/app@0.15.0) + '@firebase/messaging': 0.13.0(@firebase/app@0.15.1) '@firebase/util': 1.15.1 tslib: 2.8.1 transitivePeerDependencies: @@ -2547,22 +3087,22 @@ snapshots: '@firebase/messaging-interop-types@0.2.5': {} - '@firebase/messaging@0.13.0(@firebase/app@0.15.0)': + '@firebase/messaging@0.13.0(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) '@firebase/messaging-interop-types': 0.2.5 '@firebase/util': 1.15.1 idb: 7.1.1 tslib: 2.8.1 - '@firebase/performance-compat@0.2.25(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/performance-compat@0.2.25(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 - '@firebase/performance': 0.7.12(@firebase/app@0.15.0) + '@firebase/performance': 0.7.12(@firebase/app@0.15.1) '@firebase/performance-types': 0.2.4 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2571,22 +3111,22 @@ snapshots: '@firebase/performance-types@0.2.4': {} - '@firebase/performance@0.7.12(@firebase/app@0.15.0)': + '@firebase/performance@0.7.12(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 web-vitals: 4.2.4 - '@firebase/remote-config-compat@0.2.26(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + '@firebase/remote-config-compat@0.2.27(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 '@firebase/logger': 0.5.1 - '@firebase/remote-config': 0.8.5(@firebase/app@0.15.0) + '@firebase/remote-config': 0.9.0(@firebase/app@0.15.1) '@firebase/remote-config-types': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2595,20 +3135,20 @@ snapshots: '@firebase/remote-config-types@0.5.1': {} - '@firebase/remote-config@0.8.5(@firebase/app@0.15.0)': + '@firebase/remote-config@0.9.0(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) '@firebase/logger': 0.5.1 '@firebase/util': 1.15.1 tslib: 2.8.1 - '@firebase/storage-compat@0.4.3(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + '@firebase/storage-compat@0.4.3(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1)': dependencies: - '@firebase/app-compat': 0.5.14 + '@firebase/app-compat': 0.5.15 '@firebase/component': 0.7.3 - '@firebase/storage': 0.14.3(@firebase/app@0.15.0) + '@firebase/storage': 0.14.3(@firebase/app@0.15.1) '@firebase/storage-types': 0.8.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2621,9 +3161,9 @@ snapshots: '@firebase/app-types': 0.9.5 '@firebase/util': 1.15.1 - '@firebase/storage@0.14.3(@firebase/app@0.15.0)': + '@firebase/storage@0.14.3(@firebase/app@0.15.1)': dependencies: - '@firebase/app': 0.15.0 + '@firebase/app': 0.15.1 '@firebase/component': 0.7.3 '@firebase/util': 1.15.1 tslib: 2.8.1 @@ -2756,6 +3296,49 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + import-in-the-middle: 3.3.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + '@oxc-project/types@0.138.0': {} '@pinojs/redact@0.4.0': {} @@ -2834,6 +3417,169 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sentry/babel-plugin-component-annotate@5.3.0': {} + + '@sentry/browser-utils@10.64.0': + dependencies: + '@sentry/core': 10.64.0 + + '@sentry/browser@10.64.0': + dependencies: + '@sentry/browser-utils': 10.64.0 + '@sentry/core': 10.64.0 + '@sentry/feedback': 10.64.0 + '@sentry/replay': 10.64.0 + '@sentry/replay-canvas': 10.64.0 + + '@sentry/bundler-plugin-core@5.3.0': + dependencies: + '@babel/core': 7.29.7(supports-color@5.5.0) + '@sentry/babel-plugin-component-annotate': 5.3.0 + '@sentry/cli': 2.58.6 + dotenv: 16.6.1 + find-up: 5.0.0 + glob: 13.0.6 + magic-string: 0.30.21 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.58.6': + optional: true + + '@sentry/cli-linux-arm64@2.58.6': + optional: true + + '@sentry/cli-linux-arm@2.58.6': + optional: true + + '@sentry/cli-linux-i686@2.58.6': + optional: true + + '@sentry/cli-linux-x64@2.58.6': + optional: true + + '@sentry/cli-win32-arm64@2.58.6': + optional: true + + '@sentry/cli-win32-i686@2.58.6': + optional: true + + '@sentry/cli-win32-x64@2.58.6': + optional: true + + '@sentry/cli@2.58.6': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.58.6 + '@sentry/cli-linux-arm': 2.58.6 + '@sentry/cli-linux-arm64': 2.58.6 + '@sentry/cli-linux-i686': 2.58.6 + '@sentry/cli-linux-x64': 2.58.6 + '@sentry/cli-win32-arm64': 2.58.6 + '@sentry/cli-win32-i686': 2.58.6 + '@sentry/cli-win32-x64': 2.58.6 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/conventions@0.15.1': {} + + '@sentry/core@10.64.0': + dependencies: + '@sentry/conventions': 0.15.1 + + '@sentry/feedback@10.64.0': + dependencies: + '@sentry/core': 10.64.0 + + '@sentry/node-core@10.64.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))': + dependencies: + '@sentry/conventions': 0.15.1 + '@sentry/core': 10.64.0 + '@sentry/opentelemetry': 10.64.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + + '@sentry/node@10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.15.1 + '@sentry/core': 10.64.0 + '@sentry/node-core': 10.64.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.64.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.64.0 + import-in-the-middle: 3.3.1 + transitivePeerDependencies: + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.64.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.15.1 + '@sentry/core': 10.64.0 + + '@sentry/replay-canvas@10.64.0': + dependencies: + '@sentry/core': 10.64.0 + '@sentry/replay': 10.64.0 + + '@sentry/replay@10.64.0': + dependencies: + '@sentry/browser-utils': 10.64.0 + '@sentry/core': 10.64.0 + + '@sentry/rollup-plugin@5.3.0': + dependencies: + '@sentry/bundler-plugin-core': 5.3.0 + magic-string: 0.30.21 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/server-utils@10.64.0': + dependencies: + '@apm-js-collab/code-transformer': 0.15.0 + '@apm-js-collab/code-transformer-bundler-plugins': 0.5.0 + '@apm-js-collab/tracing-hooks': 0.10.1 + '@sentry/conventions': 0.15.1 + '@sentry/core': 10.64.0 + magic-string: 0.30.21 + transitivePeerDependencies: + - supports-color + + '@sentry/vite-plugin@5.3.0': + dependencies: + '@sentry/bundler-plugin-core': 5.3.0 + '@sentry/rollup-plugin': 5.3.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@sentry/vue@10.64.0(pinia@3.0.4(vue@3.5.39))(vue@3.5.39)': + dependencies: + '@sentry/browser': 10.64.0 + '@sentry/core': 10.64.0 + vue: 3.5.39 + optionalDependencies: + pinia: 3.0.4(vue@3.5.39) + '@swc/helpers@0.5.21': dependencies: tslib: 2.8.1 @@ -2899,12 +3645,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 - '@tailwindcss/vite@4.3.2(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.2(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.2 '@tailwindcss/oxide': 4.3.2 tailwindcss: 4.3.2 - vite: 8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) '@tanstack/table-core@8.21.3': {} @@ -2929,6 +3675,8 @@ snapshots: '@types/dom-chromium-installation-events@101.0.4': {} + '@types/estree@1.0.9': {} + '@types/jsesc@2.5.1': {} '@types/node@24.1.0': @@ -2947,10 +3695,10 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@6.0.7(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39)': + '@vitejs/plugin-vue@6.0.7(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39)': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) vue: 3.5.39 '@vue-macros/common@3.1.2(vue@3.5.39)': @@ -2998,7 +3746,7 @@ snapshots: '@vue/shared': 3.5.38 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.15 + postcss: 8.5.16 source-map-js: 1.2.1 '@vue/compiler-sfc@3.5.39': @@ -3121,6 +3869,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -3153,6 +3907,8 @@ snapshots: '@babel/types': 7.29.0 ast-kit: 2.2.0 + astring@1.9.0: {} + async@3.2.6: {} atomic-sleep@1.0.0: {} @@ -3161,6 +3917,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.42: {} + binary-extensions@2.3.0: {} birpc@2.8.0: {} @@ -3187,6 +3945,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.5: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.5) + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -3204,6 +3970,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001803: {} + chalk@4.1.1: dependencies: ansi-styles: 4.3.0 @@ -3240,6 +4008,8 @@ snapshots: circle-progress.vue@3.4.1: {} + cjs-module-lexer@2.2.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -3297,6 +4067,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.1: {} @@ -3346,6 +4118,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.389: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -3369,6 +4143,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.3.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -3377,6 +4153,12 @@ snapshots: escape-html@1.0.3: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@2.0.2: {} etag@1.8.1: {} @@ -3453,35 +4235,40 @@ snapshots: transitivePeerDependencies: - supports-color - firebase@12.15.0: + find-up@5.0.0: dependencies: - '@firebase/ai': 2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) - '@firebase/analytics': 0.10.22(@firebase/app@0.15.0) - '@firebase/analytics-compat': 0.2.28(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/app': 0.15.0 - '@firebase/app-check': 0.12.0(@firebase/app@0.15.0) - '@firebase/app-check-compat': 0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/app-compat': 0.5.14 + locate-path: 6.0.0 + path-exists: 4.0.0 + + firebase@12.16.0: + dependencies: + '@firebase/ai': 2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.1) + '@firebase/analytics': 0.10.22(@firebase/app@0.15.1) + '@firebase/analytics-compat': 0.2.28(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/app': 0.15.1 + '@firebase/app-check': 0.12.0(@firebase/app@0.15.1) + '@firebase/app-check-compat': 0.4.5(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/app-compat': 0.5.15 '@firebase/app-types': 0.9.5 - '@firebase/auth': 1.13.3(@firebase/app@0.15.0) - '@firebase/auth-compat': 0.6.8(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) - '@firebase/data-connect': 0.7.1(@firebase/app@0.15.0) + '@firebase/auth': 1.13.3(@firebase/app@0.15.1) + '@firebase/auth-compat': 0.6.8(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1) + '@firebase/data-connect': 0.7.1(@firebase/app@0.15.1) '@firebase/database': 1.1.3 '@firebase/database-compat': 2.1.4 - '@firebase/firestore': 4.16.0(@firebase/app@0.15.0) - '@firebase/firestore-compat': 0.4.11(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) - '@firebase/functions': 0.13.5(@firebase/app@0.15.0) - '@firebase/functions-compat': 0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/installations': 0.6.22(@firebase/app@0.15.0) - '@firebase/installations-compat': 0.2.22(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) - '@firebase/messaging': 0.13.0(@firebase/app@0.15.0) - '@firebase/messaging-compat': 0.2.27(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/performance': 0.7.12(@firebase/app@0.15.0) - '@firebase/performance-compat': 0.2.25(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/remote-config': 0.8.5(@firebase/app@0.15.0) - '@firebase/remote-config-compat': 0.2.26(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) - '@firebase/storage': 0.14.3(@firebase/app@0.15.0) - '@firebase/storage-compat': 0.4.3(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/firestore': 4.16.0(@firebase/app@0.15.1) + '@firebase/firestore-compat': 0.4.11(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1) + '@firebase/functions': 0.13.5(@firebase/app@0.15.1) + '@firebase/functions-compat': 0.4.5(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/installations': 0.6.22(@firebase/app@0.15.1) + '@firebase/installations-compat': 0.2.22(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1) + '@firebase/messaging': 0.13.0(@firebase/app@0.15.1) + '@firebase/messaging-compat': 0.2.27(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/performance': 0.7.12(@firebase/app@0.15.1) + '@firebase/performance-compat': 0.2.25(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/remote-config': 0.9.0(@firebase/app@0.15.1) + '@firebase/remote-config-compat': 0.2.27(@firebase/app-compat@0.5.15)(@firebase/app@0.15.1) + '@firebase/storage': 0.14.3(@firebase/app@0.15.1) + '@firebase/storage-compat': 0.4.3(@firebase/app-compat@0.5.15)(@firebase/app-types@0.9.5)(@firebase/app@0.15.1) '@firebase/util': 1.15.1 transitivePeerDependencies: - '@react-native-async-storage/async-storage' @@ -3495,6 +4282,8 @@ snapshots: function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} @@ -3521,6 +4310,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3565,6 +4360,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + httpxy@0.5.4: {} iconv-lite@0.7.1: @@ -3577,6 +4379,12 @@ snapshots: ignore-by-default@1.0.1: {} + import-in-the-middle@3.3.1: + dependencies: + cjs-module-lexer: 2.2.0 + es-module-lexer: 2.3.0 + module-details-from-path: 1.0.4 + inherits@2.0.4: {} ip-address@10.2.0: {} @@ -3605,10 +4413,14 @@ snapshots: is-what@5.5.0: {} + isexe@2.0.0: {} + jiti@2.7.0: {} joycon@3.1.1: {} + js-tokens@4.0.0: {} + jsesc@3.1.0: {} json5@2.2.3: {} @@ -3693,12 +4505,22 @@ snapshots: pkg-types: 2.3.0 quansync: 0.2.11 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} lodash@4.18.1: {} long@5.3.2: {} + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string-ast@1.0.3: dependencies: magic-string: 0.30.21 @@ -3729,6 +4551,8 @@ snapshots: merge-descriptors@2.0.0: {} + meriyah@6.1.4: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3770,6 +4594,8 @@ snapshots: mmdb-lib@3.0.2: {} + module-details-from-path@1.0.4: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -3780,6 +4606,12 @@ snapshots: negotiator@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.51: {} + nodemon@3.1.14: dependencies: chokidar: 3.6.0 @@ -3809,8 +4641,23 @@ snapshots: dependencies: wrappy: 1.0.2 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + parseurl@1.3.3: {} + path-exists@4.0.0: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + path-to-regexp@8.4.2: {} pathe@2.0.3: {} @@ -3825,6 +4672,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pinia@3.0.4(vue@3.5.39): dependencies: '@vue/devtools-api': 7.7.8 @@ -3892,12 +4741,6 @@ snapshots: transitivePeerDependencies: - supports-color - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.16: dependencies: nanoid: 3.3.12 @@ -3906,6 +4749,8 @@ snapshots: process-warning@5.0.0: {} + progress@2.0.3: {} + protobufjs@7.5.8: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -3926,6 +4771,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pstree.remy@1.1.8: {} pump@3.0.4: @@ -3982,6 +4829,13 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + rfdc@1.4.1: {} rolldown@1.1.4: @@ -4029,6 +4883,10 @@ snapshots: secure-json-parse@4.1.0: {} + semifies@1.0.0: {} + + semver@6.3.1: {} + semver@7.6.3: {} send@1.2.0(supports-color@5.5.0): @@ -4098,6 +4956,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + speakingurl@14.0.1: {} split2@4.2.0: {} @@ -4186,6 +5046,8 @@ snapshots: touch@3.1.1: {} + tr46@0.0.3: {} + tree-kill@1.2.2: {} tslib@2.8.1: {} @@ -4232,6 +5094,12 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.5): + dependencies: + browserslist: 4.28.5 + escalade: 3.2.0 + picocolors: 1.1.1 + vary@1.1.2: {} vaul-vue@0.4.1(reka-ui@2.10.1(vue@3.5.39))(vue@3.5.39): @@ -4249,10 +5117,10 @@ snapshots: core-js: 3.49.0 mutation-observer: 1.0.3 - vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 + picomatch: 4.0.5 postcss: 8.5.16 rolldown: 1.1.4 tinyglobby: 0.2.17 @@ -4279,7 +5147,7 @@ snapshots: markdown-it: 14.2.0 vue: 3.5.39 - vue-router@5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(vue@3.5.39))(vite@8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39): + vue-router@5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(vue@3.5.39))(vite@8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39): dependencies: '@babel/generator': 8.0.0-rc.6 '@vue-macros/common': 3.1.2(vue@3.5.39) @@ -4302,7 +5170,7 @@ snapshots: optionalDependencies: '@vue/compiler-sfc': 3.5.39 pinia: 3.0.4(vue@3.5.39) - vite: 8.1.3(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) vue-sonner@2.0.9: {} @@ -4316,6 +5184,8 @@ snapshots: web-vitals@4.2.4: {} + webidl-conversions@3.0.1: {} + webpack-virtual-modules@0.6.2: {} websocket-driver@0.7.4: @@ -4326,6 +5196,15 @@ snapshots: websocket-extensions@0.1.4: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + whoiser@1.18.0: dependencies: punycode: 2.3.1 @@ -4346,6 +5225,8 @@ snapshots: y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@5.0.0: {} yaml@2.9.0: {} @@ -4372,3 +5253,5 @@ snapshots: string-width: 7.2.0 y18n: 5.0.8 yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 75bb7b8c..8c1902a8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,11 @@ allowBuilds: '@firebase/util': true + '@sentry/cli': true core-js: true protobufjs: true vue-demi: true minimumReleaseAgeExclude: - - vite@8.1.0 + - vite@8.1.0 || 8.1.4 - tar@7.5.19 - '@code-inspector/core@1.6.3 || 1.6.6' - '@code-inspector/esbuild@1.6.3 || 1.6.6' @@ -30,3 +31,8 @@ minimumReleaseAgeExclude: - code-inspector-plugin@1.6.3 || 1.6.6 - tailwindcss@4.3.2 - launch-ide@1.4.5 + - '@firebase/app-compat@0.5.15' + - '@firebase/app@0.15.1' + - '@firebase/remote-config-compat@0.2.27' + - '@firebase/remote-config@0.9.0' + - firebase@12.16.0 diff --git a/sentry-instrument.js b/sentry-instrument.js new file mode 100644 index 00000000..eea8d8a8 --- /dev/null +++ b/sentry-instrument.js @@ -0,0 +1,26 @@ +// Sentry bootstrap for the Express backend. Loaded via `node --import` (see +// the dev / start scripts in package.json) so the SDK registers its ESM +// loader hooks BEFORE express & co. load — that's what makes automatic +// per-route tracing work. +// +// Gated on SENTRY_DSN_BACKEND, same philosophy as the frontend: when the env +// var is unset this module does nothing and @sentry/node is never even +// loaded. backend-server.js attaches the matching Express error handler. +import dotenv from 'dotenv'; + +dotenv.config({ quiet: true }); + +if (process.env.SENTRY_DSN_BACKEND) { + const Sentry = await import('@sentry/node'); + Sentry.init({ + dsn: process.env.SENTRY_DSN_BACKEND, + environment: process.env.SENTRY_ENVIRONMENT || 'production', + // 100% trace sampling (per-route latency / throughput / error rate under Insights) + tracesSampleRate: 1.0, + // Sentry Logs — receives warn/error/fatal lines forwarded by the + // logMethod hook in common/logger.js + enableLogs: true, + // Never attach caller IPs / headers to events (privacy tool) + sendDefaultPii: false, + }); +} diff --git a/tests/sentry-cron.test.js b/tests/sentry-cron.test.js new file mode 100644 index 00000000..8e67e041 --- /dev/null +++ b/tests/sentry-cron.test.js @@ -0,0 +1,55 @@ +// Passthrough behavior of common/sentry-cron.js when no backend DSN is set — +// the wrapper must be a transparent no-op (result and rejection both flow +// through) so periodic jobs behave identically on Sentry-less deployments. +// Also covers common/logger.js's flattenLogAttributes (the pino-context → +// Sentry-log-attributes normalizer used by the forwarding hook). +import { test } from 'node:test'; +import assert from 'node:assert'; + +// Ensure the gate is closed regardless of the shell environment, then import +// (both modules evaluate the gate once at load). +delete process.env.SENTRY_DSN_BACKEND; +const { withCronMonitor } = await import('../common/sentry-cron.js'); +const { flattenLogAttributes } = await import('../common/logger.js'); + +test('returns the job result unchanged when Sentry is not configured', async () => { + const result = await withCronMonitor('test-job', async () => 42, { + schedule: { type: 'interval', value: 5, unit: 'minute' }, + }); + assert.strictEqual(result, 42); +}); + +test('propagates a rejected job to the caller', async () => { + await assert.rejects( + () => withCronMonitor('test-job', async () => { throw new Error('boom'); }, {}), + /boom/, + ); +}); + +test('runs the job exactly once', async () => { + let calls = 0; + await withCronMonitor('test-job', async () => { calls += 1; }, {}); + assert.strictEqual(calls, 1); +}); + +test('flattens Error values into message + stack attributes', () => { + const err = new Error('kaput'); + const attrs = flattenLogAttributes({ err, ip: '1.2.3.4' }); + assert.strictEqual(attrs['err.message'], 'kaput'); + assert.ok(attrs['err.stack'].includes('kaput')); + assert.strictEqual(attrs.ip, '1.2.3.4'); + assert.strictEqual('err' in attrs, false); +}); + +test('serializes nested objects and passes primitives through', () => { + const attrs = flattenLogAttributes({ + req: { method: 'GET', url: '/api/x' }, + count: 3, + flag: true, + nothing: null, + }); + assert.strictEqual(attrs.req, '{"method":"GET","url":"/api/x"}'); + assert.strictEqual(attrs.count, 3); + assert.strictEqual(attrs.flag, true); + assert.strictEqual(attrs.nothing, null); +}); diff --git a/tests/sentry-tunnel.test.js b/tests/sentry-tunnel.test.js new file mode 100644 index 00000000..2f44dda0 --- /dev/null +++ b/tests/sentry-tunnel.test.js @@ -0,0 +1,64 @@ +// api/sentry-tunnel.js — envelope-header parsing and the guard branches that +// return before any upstream forward (method gate / unconfigured / invalid +// envelope / foreign DSN). No real Sentry traffic is ever sent. +import { test, beforeEach } from 'node:test'; +import assert from 'node:assert'; + +import tunnelHandler, { parseEnvelopeDsn } from '../api/sentry-tunnel.js'; + +const OUR_DSN = 'https://publickey@o111.ingest.sentry.io/222'; + +const makeRes = () => { + const res = { + statusCode: null, + body: null, + status(code) { this.statusCode = code; return this; }, + json(payload) { this.body = payload; return this; }, + send(payload) { this.body = payload; return this; }, + }; + return res; +}; + +const envelopeFor = (dsn) => Buffer.from(`${JSON.stringify({ dsn })}\n{"type":"event"}\n{}`); + +beforeEach(() => { + delete process.env.VITE_SENTRY_DSN_FRONTEND; +}); + +test('parseEnvelopeDsn extracts the DSN from the envelope header', () => { + const dsn = parseEnvelopeDsn(envelopeFor(OUR_DSN)); + assert.strictEqual(dsn.host, 'o111.ingest.sentry.io'); + assert.strictEqual(dsn.pathname, '/222'); +}); + +test('parseEnvelopeDsn returns null for garbage input', () => { + assert.strictEqual(parseEnvelopeDsn(Buffer.from('not json')), null); + assert.strictEqual(parseEnvelopeDsn(Buffer.from('{"no_dsn":1}\n{}')), null); +}); + +test('rejects non-POST methods with 405', async () => { + const res = makeRes(); + await tunnelHandler({ method: 'GET' }, res); + assert.strictEqual(res.statusCode, 405); +}); + +test('returns 404 when no frontend DSN is configured', async () => { + const res = makeRes(); + await tunnelHandler({ method: 'POST', body: envelopeFor(OUR_DSN) }, res); + assert.strictEqual(res.statusCode, 404); +}); + +test('returns 400 on an unparseable envelope', async () => { + process.env.VITE_SENTRY_DSN_FRONTEND = OUR_DSN; + const res = makeRes(); + await tunnelHandler({ method: 'POST', body: Buffer.from('garbage') }, res); + assert.strictEqual(res.statusCode, 400); +}); + +test('returns 403 when the envelope targets a foreign DSN', async () => { + process.env.VITE_SENTRY_DSN_FRONTEND = OUR_DSN; + const res = makeRes(); + const foreign = envelopeFor('https://otherkey@o999.ingest.sentry.io/888'); + await tunnelHandler({ method: 'POST', body: foreign }, res); + assert.strictEqual(res.statusCode, 403); +}); diff --git a/vite.config.js b/vite.config.js index 480f9410..6fb9bec5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,9 +3,14 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' import { CodeInspectorPlugin } from 'code-inspector-plugin'; +import { sentryVitePlugin } from '@sentry/vite-plugin'; dotenv.config(); +// Sentry source-map upload — build-time only, gated on SENTRY_AUTH_TOKEN +// uploaded to Sentry, then deleted from dist/ — users never download them. +const sentryUploadEnabled = !!process.env.SENTRY_AUTH_TOKEN; + const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10); const frontEndPort = parseInt(process.env.FRONTEND_PORT || 18966, 10); const nodeModuleChunkGroups = { @@ -98,6 +103,21 @@ export default defineConfig({ copy: '{file}', }, }), + // Must stay last so it sees the final emitted chunks. Release name + // defaults to the current git commit SHA; the plugin injects the same + // release into the SDK at runtime, tying events to their sourcemaps. + sentryUploadEnabled && sentryVitePlugin({ + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT_FRONTEND, + authToken: process.env.SENTRY_AUTH_TOKEN, + telemetry: false, + sourcemaps: { + // Rolldown's module-loading runtime chunk has no source to map — + // skip it so every build doesn't warn about its missing .map + ignore: ['**/rolldown-runtime*.js'], + filesToDeleteAfterUpload: ['dist/**/*.map'], + }, + }), ], resolve: { alias: { @@ -105,6 +125,9 @@ export default defineConfig({ }, }, build: { + // Hidden sourcemaps exist only for the Sentry upload; without a token, + // don't generate them at all (they'd end up publicly served from dist/) + sourcemap: sentryUploadEnabled ? 'hidden' : false, rollupOptions: { // @vueuse/core ships a couple of /* #__PURE__ */ comments in positions // Rolldown (Vite 8's new bundler) can't interpret. They're harmless —